diff options
Diffstat (limited to 'drivers/usb/chipidea')
23 files changed, 8158 insertions, 0 deletions
diff --git a/drivers/usb/chipidea/Kconfig b/drivers/usb/chipidea/Kconfig new file mode 100644 index 000000000..ee34e9046 --- /dev/null +++ b/drivers/usb/chipidea/Kconfig @@ -0,0 +1,42 @@ +config USB_CHIPIDEA + tristate "ChipIdea Highspeed Dual Role Controller" + depends on ((USB_EHCI_HCD && USB_GADGET) || (USB_EHCI_HCD && !USB_GADGET) || (!USB_EHCI_HCD && USB_GADGET)) && HAS_DMA + select EXTCON + select RESET_CONTROLLER + select USB_ULPI_BUS + help + Say Y here if your system has a dual role high speed USB + controller based on ChipIdea silicon IP. It supports: + Dual-role switch (ID, OTG FSM, sysfs), Host-only, and + Peripheral-only. + + When compiled dynamically, the module will be called ci_hdrc.ko. + +if USB_CHIPIDEA + +config USB_CHIPIDEA_OF + tristate + depends on OF + default USB_CHIPIDEA + +config USB_CHIPIDEA_PCI + tristate + depends on USB_PCI + depends on NOP_USB_XCEIV + default USB_CHIPIDEA + +config USB_CHIPIDEA_UDC + bool "ChipIdea device controller" + depends on USB_GADGET + help + Say Y here to enable device controller functionality of the + ChipIdea driver. + +config USB_CHIPIDEA_HOST + bool "ChipIdea host controller" + depends on USB_EHCI_HCD + select USB_EHCI_ROOT_HUB_TT + help + Say Y here to enable host controller functionality of the + ChipIdea driver. +endif diff --git a/drivers/usb/chipidea/Makefile b/drivers/usb/chipidea/Makefile new file mode 100644 index 000000000..12df94f78 --- /dev/null +++ b/drivers/usb/chipidea/Makefile @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_USB_CHIPIDEA) += ci_hdrc.o + +ci_hdrc-y := core.o otg.o debug.o ulpi.o +ci_hdrc-$(CONFIG_USB_CHIPIDEA_UDC) += udc.o +ci_hdrc-$(CONFIG_USB_CHIPIDEA_HOST) += host.o +ci_hdrc-$(CONFIG_USB_OTG_FSM) += otg_fsm.o + +# Glue/Bridge layers go here + +obj-$(CONFIG_USB_CHIPIDEA) += ci_hdrc_usb2.o +obj-$(CONFIG_USB_CHIPIDEA) += ci_hdrc_msm.o +obj-$(CONFIG_USB_CHIPIDEA) += ci_hdrc_zevio.o + +obj-$(CONFIG_USB_CHIPIDEA_PCI) += ci_hdrc_pci.o + +obj-$(CONFIG_USB_CHIPIDEA_OF) += usbmisc_imx.o ci_hdrc_imx.o +obj-$(CONFIG_USB_CHIPIDEA_OF) += ci_hdrc_tegra.o diff --git a/drivers/usb/chipidea/bits.h b/drivers/usb/chipidea/bits.h new file mode 100644 index 000000000..98da99510 --- /dev/null +++ b/drivers/usb/chipidea/bits.h @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * bits.h - register bits of the ChipIdea USB IP core + * + * Copyright (C) 2008 Chipidea - MIPS Technologies, Inc. All rights reserved. + * + * Author: David Lopo + */ + +#ifndef __DRIVERS_USB_CHIPIDEA_BITS_H +#define __DRIVERS_USB_CHIPIDEA_BITS_H + +#include <linux/usb/ehci_def.h> + +/* + * ID + * For 1.x revision, bit24 - bit31 are reserved + * For 2.x revision, bit25 - bit28 are 0x2 + */ +#define TAG (0x1F << 16) +#define REVISION (0xF << 21) +#define VERSION (0xF << 25) +#define CIVERSION (0x7 << 29) + +/* SBUSCFG */ +#define AHBBRST_MASK 0x7 + +/* HCCPARAMS */ +#define HCCPARAMS_LEN BIT(17) + +/* DCCPARAMS */ +#define DCCPARAMS_DEN (0x1F << 0) +#define DCCPARAMS_DC BIT(7) +#define DCCPARAMS_HC BIT(8) + +/* TESTMODE */ +#define TESTMODE_FORCE BIT(0) + +/* USBCMD */ +#define USBCMD_RS BIT(0) +#define USBCMD_RST BIT(1) +#define USBCMD_SUTW BIT(13) +#define USBCMD_ATDTW BIT(14) + +/* USBSTS & USBINTR */ +#define USBi_UI BIT(0) +#define USBi_UEI BIT(1) +#define USBi_PCI BIT(2) +#define USBi_URI BIT(6) +#define USBi_SLI BIT(8) + +/* DEVICEADDR */ +#define DEVICEADDR_USBADRA BIT(24) +#define DEVICEADDR_USBADR (0x7FUL << 25) + +/* TTCTRL */ +#define TTCTRL_TTHA_MASK (0x7fUL << 24) +/* Set non-zero value for internal TT Hub address representation */ +#define TTCTRL_TTHA (0x7fUL << 24) + +/* BURSTSIZE */ +#define RX_BURST_MASK 0xff +#define TX_BURST_MASK 0xff00 + +/* PORTSC */ +#define PORTSC_CCS BIT(0) +#define PORTSC_CSC BIT(1) +#define PORTSC_PEC BIT(3) +#define PORTSC_OCC BIT(5) +#define PORTSC_FPR BIT(6) +#define PORTSC_SUSP BIT(7) +#define PORTSC_HSP BIT(9) +#define PORTSC_PP BIT(12) +#define PORTSC_PTC (0x0FUL << 16) +#define PORTSC_WKCN BIT(20) +#define PORTSC_PHCD(d) ((d) ? BIT(22) : BIT(23)) +/* PTS and PTW for non lpm version only */ +#define PORTSC_PFSC BIT(24) +#define PORTSC_PTS(d) \ + (u32)((((d) & 0x3) << 30) | (((d) & 0x4) ? BIT(25) : 0)) +#define PORTSC_PTW BIT(28) +#define PORTSC_STS BIT(29) + +#define PORTSC_W1C_BITS \ + (PORTSC_CSC | PORTSC_PEC | PORTSC_OCC) + +/* DEVLC */ +#define DEVLC_PFSC BIT(23) +#define DEVLC_PSPD (0x03UL << 25) +#define DEVLC_PSPD_HS (0x02UL << 25) +#define DEVLC_PTW BIT(27) +#define DEVLC_STS BIT(28) +#define DEVLC_PTS(d) (u32)(((d) & 0x7) << 29) + +/* Encoding for DEVLC_PTS and PORTSC_PTS */ +#define PTS_UTMI 0 +#define PTS_ULPI 2 +#define PTS_SERIAL 3 +#define PTS_HSIC 4 + +/* OTGSC */ +#define OTGSC_IDPU BIT(5) +#define OTGSC_HADP BIT(6) +#define OTGSC_HABA BIT(7) +#define OTGSC_ID BIT(8) +#define OTGSC_AVV BIT(9) +#define OTGSC_ASV BIT(10) +#define OTGSC_BSV BIT(11) +#define OTGSC_BSE BIT(12) +#define OTGSC_IDIS BIT(16) +#define OTGSC_AVVIS BIT(17) +#define OTGSC_ASVIS BIT(18) +#define OTGSC_BSVIS BIT(19) +#define OTGSC_BSEIS BIT(20) +#define OTGSC_1MSIS BIT(21) +#define OTGSC_DPIS BIT(22) +#define OTGSC_IDIE BIT(24) +#define OTGSC_AVVIE BIT(25) +#define OTGSC_ASVIE BIT(26) +#define OTGSC_BSVIE BIT(27) +#define OTGSC_BSEIE BIT(28) +#define OTGSC_1MSIE BIT(29) +#define OTGSC_DPIE BIT(30) +#define OTGSC_INT_EN_BITS (OTGSC_IDIE | OTGSC_AVVIE | OTGSC_ASVIE \ + | OTGSC_BSVIE | OTGSC_BSEIE | OTGSC_1MSIE \ + | OTGSC_DPIE) +#define OTGSC_INT_STATUS_BITS (OTGSC_IDIS | OTGSC_AVVIS | OTGSC_ASVIS \ + | OTGSC_BSVIS | OTGSC_BSEIS | OTGSC_1MSIS \ + | OTGSC_DPIS) + +/* USBMODE */ +#define USBMODE_CM (0x03UL << 0) +#define USBMODE_CM_DC (0x02UL << 0) +#define USBMODE_SLOM BIT(3) +#define USBMODE_CI_SDIS BIT(4) + +/* ENDPTCTRL */ +#define ENDPTCTRL_RXS BIT(0) +#define ENDPTCTRL_RXT (0x03UL << 2) +#define ENDPTCTRL_RXR BIT(6) /* reserved for port 0 */ +#define ENDPTCTRL_RXE BIT(7) +#define ENDPTCTRL_TXS BIT(16) +#define ENDPTCTRL_TXT (0x03UL << 18) +#define ENDPTCTRL_TXR BIT(22) /* reserved for port 0 */ +#define ENDPTCTRL_TXE BIT(23) + +#endif /* __DRIVERS_USB_CHIPIDEA_BITS_H */ diff --git a/drivers/usb/chipidea/ci.h b/drivers/usb/chipidea/ci.h new file mode 100644 index 000000000..6a2cc5cd0 --- /dev/null +++ b/drivers/usb/chipidea/ci.h @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ci.h - common structures, functions, and macros of the ChipIdea driver + * + * Copyright (C) 2008 Chipidea - MIPS Technologies, Inc. All rights reserved. + * + * Author: David Lopo + */ + +#ifndef __DRIVERS_USB_CHIPIDEA_CI_H +#define __DRIVERS_USB_CHIPIDEA_CI_H + +#include <linux/list.h> +#include <linux/irqreturn.h> +#include <linux/usb.h> +#include <linux/usb/gadget.h> +#include <linux/usb/otg-fsm.h> +#include <linux/usb/otg.h> +#include <linux/ulpi/interface.h> + +/****************************************************************************** + * DEFINE + *****************************************************************************/ +#define TD_PAGE_COUNT 5 +#define CI_HDRC_PAGE_SIZE 4096ul /* page size for TD's */ +#define ENDPT_MAX 32 + +/****************************************************************************** + * REGISTERS + *****************************************************************************/ +/* Identification Registers */ +#define ID_ID 0x0 +#define ID_HWGENERAL 0x4 +#define ID_HWHOST 0x8 +#define ID_HWDEVICE 0xc +#define ID_HWTXBUF 0x10 +#define ID_HWRXBUF 0x14 +#define ID_SBUSCFG 0x90 + +/* register indices */ +enum ci_hw_regs { + CAP_CAPLENGTH, + CAP_HCCPARAMS, + CAP_DCCPARAMS, + CAP_TESTMODE, + CAP_LAST = CAP_TESTMODE, + OP_USBCMD, + OP_USBSTS, + OP_USBINTR, + OP_DEVICEADDR, + OP_ENDPTLISTADDR, + OP_TTCTRL, + OP_BURSTSIZE, + OP_ULPI_VIEWPORT, + OP_PORTSC, + OP_DEVLC, + OP_OTGSC, + OP_USBMODE, + OP_ENDPTSETUPSTAT, + OP_ENDPTPRIME, + OP_ENDPTFLUSH, + OP_ENDPTSTAT, + OP_ENDPTCOMPLETE, + OP_ENDPTCTRL, + /* endptctrl1..15 follow */ + OP_LAST = OP_ENDPTCTRL + ENDPT_MAX / 2, +}; + +/****************************************************************************** + * STRUCTURES + *****************************************************************************/ +/** + * struct ci_hw_ep - endpoint representation + * @ep: endpoint structure for gadget drivers + * @dir: endpoint direction (TX/RX) + * @num: endpoint number + * @type: endpoint type + * @name: string description of the endpoint + * @qh: queue head for this endpoint + * @wedge: is the endpoint wedged + * @ci: pointer to the controller + * @lock: pointer to controller's spinlock + * @td_pool: pointer to controller's TD pool + */ +struct ci_hw_ep { + struct usb_ep ep; + u8 dir; + u8 num; + u8 type; + char name[16]; + struct { + struct list_head queue; + struct ci_hw_qh *ptr; + dma_addr_t dma; + } qh; + int wedge; + + /* global resources */ + struct ci_hdrc *ci; + spinlock_t *lock; + struct dma_pool *td_pool; + struct td_node *pending_td; +}; + +enum ci_role { + CI_ROLE_HOST = 0, + CI_ROLE_GADGET, + CI_ROLE_END, +}; + +enum ci_revision { + CI_REVISION_1X = 10, /* Revision 1.x */ + CI_REVISION_20 = 20, /* Revision 2.0 */ + CI_REVISION_21, /* Revision 2.1 */ + CI_REVISION_22, /* Revision 2.2 */ + CI_REVISION_23, /* Revision 2.3 */ + CI_REVISION_24, /* Revision 2.4 */ + CI_REVISION_25, /* Revision 2.5 */ + CI_REVISION_25_PLUS, /* Revision above than 2.5 */ + CI_REVISION_UNKNOWN = 99, /* Unknown Revision */ +}; + +/** + * struct ci_role_driver - host/gadget role driver + * @start: start this role + * @stop: stop this role + * @irq: irq handler for this role + * @name: role name string (host/gadget) + */ +struct ci_role_driver { + int (*start)(struct ci_hdrc *); + void (*stop)(struct ci_hdrc *); + irqreturn_t (*irq)(struct ci_hdrc *); + const char *name; +}; + +/** + * struct hw_bank - hardware register mapping representation + * @lpm: set if the device is LPM capable + * @phys: physical address of the controller's registers + * @abs: absolute address of the beginning of register window + * @cap: capability registers + * @op: operational registers + * @size: size of the register window + * @regmap: register lookup table + */ +struct hw_bank { + unsigned lpm; + resource_size_t phys; + void __iomem *abs; + void __iomem *cap; + void __iomem *op; + size_t size; + void __iomem *regmap[OP_LAST + 1]; +}; + +/** + * struct ci_hdrc - chipidea device representation + * @dev: pointer to parent device + * @lock: access synchronization + * @hw_bank: hardware register mapping + * @irq: IRQ number + * @roles: array of supported roles for this controller + * @role: current role + * @is_otg: if the device is otg-capable + * @fsm: otg finite state machine + * @otg_fsm_hrtimer: hrtimer for otg fsm timers + * @hr_timeouts: time out list for active otg fsm timers + * @enabled_otg_timer_bits: bits of enabled otg timers + * @next_otg_timer: next nearest enabled timer to be expired + * @work: work for role changing + * @wq: workqueue thread + * @qh_pool: allocation pool for queue heads + * @td_pool: allocation pool for transfer descriptors + * @gadget: device side representation for peripheral controller + * @driver: gadget driver + * @resume_state: save the state of gadget suspend from + * @hw_ep_max: total number of endpoints supported by hardware + * @ci_hw_ep: array of endpoints + * @ep0_dir: ep0 direction + * @ep0out: pointer to ep0 OUT endpoint + * @ep0in: pointer to ep0 IN endpoint + * @status: ep0 status request + * @setaddr: if we should set the address on status completion + * @address: usb address received from the host + * @remote_wakeup: host-enabled remote wakeup + * @suspended: suspended by host + * @test_mode: the selected test mode + * @platdata: platform specific information supplied by parent device + * @vbus_active: is VBUS active + * @ulpi: pointer to ULPI device, if any + * @ulpi_ops: ULPI read/write ops for this device + * @phy: pointer to PHY, if any + * @usb_phy: pointer to USB PHY, if any and if using the USB PHY framework + * @hcd: pointer to usb_hcd for ehci host driver + * @debugfs: root dentry for this controller in debugfs + * @id_event: indicates there is an id event, and handled at ci_otg_work + * @b_sess_valid_event: indicates there is a vbus event, and handled + * at ci_otg_work + * @imx28_write_fix: Freescale imx28 needs swp instruction for writing + * @supports_runtime_pm: if runtime pm is supported + * @in_lpm: if the core in low power mode + * @wakeup_int: if wakeup interrupt occur + * @rev: The revision number for controller + */ +struct ci_hdrc { + struct device *dev; + spinlock_t lock; + struct hw_bank hw_bank; + int irq; + struct ci_role_driver *roles[CI_ROLE_END]; + enum ci_role role; + bool is_otg; + struct usb_otg otg; + struct otg_fsm fsm; + struct hrtimer otg_fsm_hrtimer; + ktime_t hr_timeouts[NUM_OTG_FSM_TIMERS]; + unsigned enabled_otg_timer_bits; + enum otg_fsm_timer next_otg_timer; + struct work_struct work; + struct workqueue_struct *wq; + + struct dma_pool *qh_pool; + struct dma_pool *td_pool; + + struct usb_gadget gadget; + struct usb_gadget_driver *driver; + enum usb_device_state resume_state; + unsigned hw_ep_max; + struct ci_hw_ep ci_hw_ep[ENDPT_MAX]; + u32 ep0_dir; + struct ci_hw_ep *ep0out, *ep0in; + + struct usb_request *status; + bool setaddr; + u8 address; + u8 remote_wakeup; + u8 suspended; + u8 test_mode; + + struct ci_hdrc_platform_data *platdata; + int vbus_active; + struct ulpi *ulpi; + struct ulpi_ops ulpi_ops; + struct phy *phy; + /* old usb_phy interface */ + struct usb_phy *usb_phy; + struct usb_hcd *hcd; + struct dentry *debugfs; + bool id_event; + bool b_sess_valid_event; + bool imx28_write_fix; + bool supports_runtime_pm; + bool in_lpm; + bool wakeup_int; + enum ci_revision rev; +}; + +static inline struct ci_role_driver *ci_role(struct ci_hdrc *ci) +{ + BUG_ON(ci->role >= CI_ROLE_END || !ci->roles[ci->role]); + return ci->roles[ci->role]; +} + +static inline int ci_role_start(struct ci_hdrc *ci, enum ci_role role) +{ + int ret; + + if (role >= CI_ROLE_END) + return -EINVAL; + + if (!ci->roles[role]) + return -ENXIO; + + ret = ci->roles[role]->start(ci); + if (!ret) + ci->role = role; + return ret; +} + +static inline void ci_role_stop(struct ci_hdrc *ci) +{ + enum ci_role role = ci->role; + + if (role == CI_ROLE_END) + return; + + ci->role = CI_ROLE_END; + + ci->roles[role]->stop(ci); +} + +/** + * hw_read_id_reg: reads from a identification register + * @ci: the controller + * @offset: offset from the beginning of identification registers region + * @mask: bitfield mask + * + * This function returns register contents + */ +static inline u32 hw_read_id_reg(struct ci_hdrc *ci, u32 offset, u32 mask) +{ + return ioread32(ci->hw_bank.abs + offset) & mask; +} + +/** + * hw_write_id_reg: writes to a identification register + * @ci: the controller + * @offset: offset from the beginning of identification registers region + * @mask: bitfield mask + * @data: new value + */ +static inline void hw_write_id_reg(struct ci_hdrc *ci, u32 offset, + u32 mask, u32 data) +{ + if (~mask) + data = (ioread32(ci->hw_bank.abs + offset) & ~mask) + | (data & mask); + + iowrite32(data, ci->hw_bank.abs + offset); +} + +/** + * hw_read: reads from a hw register + * @ci: the controller + * @reg: register index + * @mask: bitfield mask + * + * This function returns register contents + */ +static inline u32 hw_read(struct ci_hdrc *ci, enum ci_hw_regs reg, u32 mask) +{ + return ioread32(ci->hw_bank.regmap[reg]) & mask; +} + +#ifdef CONFIG_SOC_IMX28 +static inline void imx28_ci_writel(u32 val, volatile void __iomem *addr) +{ + __asm__ ("swp %0, %0, [%1]" : : "r"(val), "r"(addr)); +} +#else +static inline void imx28_ci_writel(u32 val, volatile void __iomem *addr) +{ +} +#endif + +static inline void __hw_write(struct ci_hdrc *ci, u32 val, + void __iomem *addr) +{ + if (ci->imx28_write_fix) + imx28_ci_writel(val, addr); + else + iowrite32(val, addr); +} + +/** + * hw_write: writes to a hw register + * @ci: the controller + * @reg: register index + * @mask: bitfield mask + * @data: new value + */ +static inline void hw_write(struct ci_hdrc *ci, enum ci_hw_regs reg, + u32 mask, u32 data) +{ + if (~mask) + data = (ioread32(ci->hw_bank.regmap[reg]) & ~mask) + | (data & mask); + + __hw_write(ci, data, ci->hw_bank.regmap[reg]); +} + +/** + * hw_test_and_clear: tests & clears a hw register + * @ci: the controller + * @reg: register index + * @mask: bitfield mask + * + * This function returns register contents + */ +static inline u32 hw_test_and_clear(struct ci_hdrc *ci, enum ci_hw_regs reg, + u32 mask) +{ + u32 val = ioread32(ci->hw_bank.regmap[reg]) & mask; + + __hw_write(ci, val, ci->hw_bank.regmap[reg]); + return val; +} + +/** + * hw_test_and_write: tests & writes a hw register + * @ci: the controller + * @reg: register index + * @mask: bitfield mask + * @data: new value + * + * This function returns register contents + */ +static inline u32 hw_test_and_write(struct ci_hdrc *ci, enum ci_hw_regs reg, + u32 mask, u32 data) +{ + u32 val = hw_read(ci, reg, ~0); + + hw_write(ci, reg, mask, data); + return (val & mask) >> __ffs(mask); +} + +/** + * ci_otg_is_fsm_mode: runtime check if otg controller + * is in otg fsm mode. + * + * @ci: chipidea device + */ +static inline bool ci_otg_is_fsm_mode(struct ci_hdrc *ci) +{ +#ifdef CONFIG_USB_OTG_FSM + struct usb_otg_caps *otg_caps = &ci->platdata->ci_otg_caps; + + return ci->is_otg && ci->roles[CI_ROLE_HOST] && + ci->roles[CI_ROLE_GADGET] && (otg_caps->srp_support || + otg_caps->hnp_support || otg_caps->adp_support); +#else + return false; +#endif +} + +int ci_ulpi_init(struct ci_hdrc *ci); +void ci_ulpi_exit(struct ci_hdrc *ci); +int ci_ulpi_resume(struct ci_hdrc *ci); + +u32 hw_read_intr_enable(struct ci_hdrc *ci); + +u32 hw_read_intr_status(struct ci_hdrc *ci); + +int hw_device_reset(struct ci_hdrc *ci); + +int hw_port_test_set(struct ci_hdrc *ci, u8 mode); + +u8 hw_port_test_get(struct ci_hdrc *ci); + +void hw_phymode_configure(struct ci_hdrc *ci); + +void ci_platform_configure(struct ci_hdrc *ci); + +void dbg_create_files(struct ci_hdrc *ci); + +void dbg_remove_files(struct ci_hdrc *ci); +#endif /* __DRIVERS_USB_CHIPIDEA_CI_H */ diff --git a/drivers/usb/chipidea/ci_hdrc_imx.c b/drivers/usb/chipidea/ci_hdrc_imx.c new file mode 100644 index 000000000..af420ec2a --- /dev/null +++ b/drivers/usb/chipidea/ci_hdrc_imx.c @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2012 Freescale Semiconductor, Inc. + * Copyright (C) 2012 Marek Vasut <marex@denx.de> + * on behalf of DENX Software Engineering GmbH + */ + +#include <linux/module.h> +#include <linux/of_platform.h> +#include <linux/of_gpio.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/dma-mapping.h> +#include <linux/usb/chipidea.h> +#include <linux/usb/of.h> +#include <linux/clk.h> + +#include "ci.h" +#include "ci_hdrc_imx.h" + +struct ci_hdrc_imx_platform_flag { + unsigned int flags; +}; + +static const struct ci_hdrc_imx_platform_flag imx23_usb_data = { + .flags = CI_HDRC_TURN_VBUS_EARLY_ON | + CI_HDRC_DISABLE_STREAMING, +}; + +static const struct ci_hdrc_imx_platform_flag imx27_usb_data = { + .flags = CI_HDRC_DISABLE_STREAMING, +}; + +static const struct ci_hdrc_imx_platform_flag imx28_usb_data = { + .flags = CI_HDRC_IMX28_WRITE_FIX | + CI_HDRC_TURN_VBUS_EARLY_ON | + CI_HDRC_DISABLE_STREAMING, +}; + +static const struct ci_hdrc_imx_platform_flag imx6q_usb_data = { + .flags = CI_HDRC_SUPPORTS_RUNTIME_PM | + CI_HDRC_TURN_VBUS_EARLY_ON | + CI_HDRC_DISABLE_STREAMING, +}; + +static const struct ci_hdrc_imx_platform_flag imx6sl_usb_data = { + .flags = CI_HDRC_SUPPORTS_RUNTIME_PM | + CI_HDRC_TURN_VBUS_EARLY_ON | + CI_HDRC_DISABLE_HOST_STREAMING, +}; + +static const struct ci_hdrc_imx_platform_flag imx6sx_usb_data = { + .flags = CI_HDRC_SUPPORTS_RUNTIME_PM | + CI_HDRC_TURN_VBUS_EARLY_ON | + CI_HDRC_DISABLE_HOST_STREAMING, +}; + +static const struct ci_hdrc_imx_platform_flag imx6ul_usb_data = { + .flags = CI_HDRC_SUPPORTS_RUNTIME_PM | + CI_HDRC_TURN_VBUS_EARLY_ON | + CI_HDRC_DISABLE_DEVICE_STREAMING, +}; + +static const struct ci_hdrc_imx_platform_flag imx7d_usb_data = { + .flags = CI_HDRC_SUPPORTS_RUNTIME_PM, +}; + +static const struct of_device_id ci_hdrc_imx_dt_ids[] = { + { .compatible = "fsl,imx23-usb", .data = &imx23_usb_data}, + { .compatible = "fsl,imx28-usb", .data = &imx28_usb_data}, + { .compatible = "fsl,imx27-usb", .data = &imx27_usb_data}, + { .compatible = "fsl,imx6q-usb", .data = &imx6q_usb_data}, + { .compatible = "fsl,imx6sl-usb", .data = &imx6sl_usb_data}, + { .compatible = "fsl,imx6sx-usb", .data = &imx6sx_usb_data}, + { .compatible = "fsl,imx6ul-usb", .data = &imx6ul_usb_data}, + { .compatible = "fsl,imx7d-usb", .data = &imx7d_usb_data}, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, ci_hdrc_imx_dt_ids); + +struct ci_hdrc_imx_data { + struct usb_phy *phy; + struct platform_device *ci_pdev; + struct clk *clk; + struct imx_usbmisc_data *usbmisc_data; + bool supports_runtime_pm; + bool override_phy_control; + bool in_lpm; + /* SoC before i.mx6 (except imx23/imx28) needs three clks */ + bool need_three_clks; + struct clk *clk_ipg; + struct clk *clk_ahb; + struct clk *clk_per; + /* --------------------------------- */ +}; + +/* Common functions shared by usbmisc drivers */ + +static struct imx_usbmisc_data *usbmisc_get_init_data(struct device *dev) +{ + struct platform_device *misc_pdev; + struct device_node *np = dev->of_node; + struct of_phandle_args args; + struct imx_usbmisc_data *data; + int ret; + + /* + * In case the fsl,usbmisc property is not present this device doesn't + * need usbmisc. Return NULL (which is no error here) + */ + if (!of_get_property(np, "fsl,usbmisc", NULL)) + return NULL; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return ERR_PTR(-ENOMEM); + + ret = of_parse_phandle_with_args(np, "fsl,usbmisc", "#index-cells", + 0, &args); + if (ret) { + dev_err(dev, "Failed to parse property fsl,usbmisc, errno %d\n", + ret); + return ERR_PTR(ret); + } + + data->index = args.args[0]; + + misc_pdev = of_find_device_by_node(args.np); + of_node_put(args.np); + + if (!misc_pdev) + return ERR_PTR(-EPROBE_DEFER); + + if (!platform_get_drvdata(misc_pdev)) { + put_device(&misc_pdev->dev); + return ERR_PTR(-EPROBE_DEFER); + } + data->dev = &misc_pdev->dev; + + if (of_find_property(np, "disable-over-current", NULL)) + data->disable_oc = 1; + + if (of_find_property(np, "over-current-active-high", NULL)) + data->oc_polarity = 1; + + if (of_find_property(np, "external-vbus-divider", NULL)) + data->evdo = 1; + + if (of_usb_get_phy_mode(np) == USBPHY_INTERFACE_MODE_ULPI) + data->ulpi = 1; + + return data; +} + +/* End of common functions shared by usbmisc drivers*/ +static int imx_get_clks(struct device *dev) +{ + struct ci_hdrc_imx_data *data = dev_get_drvdata(dev); + int ret = 0; + + data->clk_ipg = devm_clk_get(dev, "ipg"); + if (IS_ERR(data->clk_ipg)) { + /* If the platform only needs one clocks */ + data->clk = devm_clk_get(dev, NULL); + if (IS_ERR(data->clk)) { + ret = PTR_ERR(data->clk); + dev_err(dev, + "Failed to get clks, err=%ld,%ld\n", + PTR_ERR(data->clk), PTR_ERR(data->clk_ipg)); + return ret; + } + return ret; + } + + data->clk_ahb = devm_clk_get(dev, "ahb"); + if (IS_ERR(data->clk_ahb)) { + ret = PTR_ERR(data->clk_ahb); + dev_err(dev, + "Failed to get ahb clock, err=%d\n", ret); + return ret; + } + + data->clk_per = devm_clk_get(dev, "per"); + if (IS_ERR(data->clk_per)) { + ret = PTR_ERR(data->clk_per); + dev_err(dev, + "Failed to get per clock, err=%d\n", ret); + return ret; + } + + data->need_three_clks = true; + return ret; +} + +static int imx_prepare_enable_clks(struct device *dev) +{ + struct ci_hdrc_imx_data *data = dev_get_drvdata(dev); + int ret = 0; + + if (data->need_three_clks) { + ret = clk_prepare_enable(data->clk_ipg); + if (ret) { + dev_err(dev, + "Failed to prepare/enable ipg clk, err=%d\n", + ret); + return ret; + } + + ret = clk_prepare_enable(data->clk_ahb); + if (ret) { + dev_err(dev, + "Failed to prepare/enable ahb clk, err=%d\n", + ret); + clk_disable_unprepare(data->clk_ipg); + return ret; + } + + ret = clk_prepare_enable(data->clk_per); + if (ret) { + dev_err(dev, + "Failed to prepare/enable per clk, err=%d\n", + ret); + clk_disable_unprepare(data->clk_ahb); + clk_disable_unprepare(data->clk_ipg); + return ret; + } + } else { + ret = clk_prepare_enable(data->clk); + if (ret) { + dev_err(dev, + "Failed to prepare/enable clk, err=%d\n", + ret); + return ret; + } + } + + return ret; +} + +static void imx_disable_unprepare_clks(struct device *dev) +{ + struct ci_hdrc_imx_data *data = dev_get_drvdata(dev); + + if (data->need_three_clks) { + clk_disable_unprepare(data->clk_per); + clk_disable_unprepare(data->clk_ahb); + clk_disable_unprepare(data->clk_ipg); + } else { + clk_disable_unprepare(data->clk); + } +} + +static int ci_hdrc_imx_probe(struct platform_device *pdev) +{ + struct ci_hdrc_imx_data *data; + struct ci_hdrc_platform_data pdata = { + .name = dev_name(&pdev->dev), + .capoffset = DEF_CAPOFFSET, + }; + int ret; + const struct of_device_id *of_id; + const struct ci_hdrc_imx_platform_flag *imx_platform_flag; + struct device_node *np = pdev->dev.of_node; + + of_id = of_match_device(ci_hdrc_imx_dt_ids, &pdev->dev); + if (!of_id) + return -ENODEV; + + imx_platform_flag = of_id->data; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + platform_set_drvdata(pdev, data); + data->usbmisc_data = usbmisc_get_init_data(&pdev->dev); + if (IS_ERR(data->usbmisc_data)) + return PTR_ERR(data->usbmisc_data); + + ret = imx_get_clks(&pdev->dev); + if (ret) + return ret; + + ret = imx_prepare_enable_clks(&pdev->dev); + if (ret) + return ret; + + data->phy = devm_usb_get_phy_by_phandle(&pdev->dev, "fsl,usbphy", 0); + if (IS_ERR(data->phy)) { + ret = PTR_ERR(data->phy); + /* Return -EINVAL if no usbphy is available */ + if (ret == -ENODEV) + ret = -EINVAL; + goto err_clk; + } + + pdata.usb_phy = data->phy; + + if ((of_device_is_compatible(np, "fsl,imx53-usb") || + of_device_is_compatible(np, "fsl,imx51-usb")) && pdata.usb_phy && + of_usb_get_phy_mode(np) == USBPHY_INTERFACE_MODE_ULPI) { + pdata.flags |= CI_HDRC_OVERRIDE_PHY_CONTROL; + data->override_phy_control = true; + usb_phy_init(pdata.usb_phy); + } + + pdata.flags |= imx_platform_flag->flags; + if (pdata.flags & CI_HDRC_SUPPORTS_RUNTIME_PM) + data->supports_runtime_pm = true; + + ret = imx_usbmisc_init(data->usbmisc_data); + if (ret) { + dev_err(&pdev->dev, "usbmisc init failed, ret=%d\n", ret); + goto err_clk; + } + + data->ci_pdev = ci_hdrc_add_device(&pdev->dev, + pdev->resource, pdev->num_resources, + &pdata); + if (IS_ERR(data->ci_pdev)) { + ret = PTR_ERR(data->ci_pdev); + if (ret != -EPROBE_DEFER) + dev_err(&pdev->dev, + "ci_hdrc_add_device failed, err=%d\n", ret); + goto err_clk; + } + + ret = imx_usbmisc_init_post(data->usbmisc_data); + if (ret) { + dev_err(&pdev->dev, "usbmisc post failed, ret=%d\n", ret); + goto disable_device; + } + + if (data->supports_runtime_pm) { + pm_runtime_set_active(&pdev->dev); + pm_runtime_enable(&pdev->dev); + } + + device_set_wakeup_capable(&pdev->dev, true); + + return 0; + +disable_device: + ci_hdrc_remove_device(data->ci_pdev); +err_clk: + imx_disable_unprepare_clks(&pdev->dev); + return ret; +} + +static int ci_hdrc_imx_remove(struct platform_device *pdev) +{ + struct ci_hdrc_imx_data *data = platform_get_drvdata(pdev); + + if (data->supports_runtime_pm) { + pm_runtime_get_sync(&pdev->dev); + pm_runtime_disable(&pdev->dev); + pm_runtime_put_noidle(&pdev->dev); + } + ci_hdrc_remove_device(data->ci_pdev); + if (data->override_phy_control) + usb_phy_shutdown(data->phy); + imx_disable_unprepare_clks(&pdev->dev); + + return 0; +} + +static void ci_hdrc_imx_shutdown(struct platform_device *pdev) +{ + ci_hdrc_imx_remove(pdev); +} + +#ifdef CONFIG_PM +static int imx_controller_suspend(struct device *dev) +{ + struct ci_hdrc_imx_data *data = dev_get_drvdata(dev); + + dev_dbg(dev, "at %s\n", __func__); + + imx_disable_unprepare_clks(dev); + data->in_lpm = true; + + return 0; +} + +static int imx_controller_resume(struct device *dev) +{ + struct ci_hdrc_imx_data *data = dev_get_drvdata(dev); + int ret = 0; + + dev_dbg(dev, "at %s\n", __func__); + + if (!data->in_lpm) { + WARN_ON(1); + return 0; + } + + ret = imx_prepare_enable_clks(dev); + if (ret) + return ret; + + data->in_lpm = false; + + ret = imx_usbmisc_set_wakeup(data->usbmisc_data, false); + if (ret) { + dev_err(dev, "usbmisc set_wakeup failed, ret=%d\n", ret); + goto clk_disable; + } + + return 0; + +clk_disable: + imx_disable_unprepare_clks(dev); + return ret; +} + +#ifdef CONFIG_PM_SLEEP +static int ci_hdrc_imx_suspend(struct device *dev) +{ + int ret; + + struct ci_hdrc_imx_data *data = dev_get_drvdata(dev); + + if (data->in_lpm) + /* The core's suspend doesn't run */ + return 0; + + if (device_may_wakeup(dev)) { + ret = imx_usbmisc_set_wakeup(data->usbmisc_data, true); + if (ret) { + dev_err(dev, "usbmisc set_wakeup failed, ret=%d\n", + ret); + return ret; + } + } + + return imx_controller_suspend(dev); +} + +static int ci_hdrc_imx_resume(struct device *dev) +{ + struct ci_hdrc_imx_data *data = dev_get_drvdata(dev); + int ret; + + ret = imx_controller_resume(dev); + if (!ret && data->supports_runtime_pm) { + pm_runtime_disable(dev); + pm_runtime_set_active(dev); + pm_runtime_enable(dev); + } + + return ret; +} +#endif /* CONFIG_PM_SLEEP */ + +static int ci_hdrc_imx_runtime_suspend(struct device *dev) +{ + struct ci_hdrc_imx_data *data = dev_get_drvdata(dev); + int ret; + + if (data->in_lpm) { + WARN_ON(1); + return 0; + } + + ret = imx_usbmisc_set_wakeup(data->usbmisc_data, true); + if (ret) { + dev_err(dev, "usbmisc set_wakeup failed, ret=%d\n", ret); + return ret; + } + + return imx_controller_suspend(dev); +} + +static int ci_hdrc_imx_runtime_resume(struct device *dev) +{ + return imx_controller_resume(dev); +} + +#endif /* CONFIG_PM */ + +static const struct dev_pm_ops ci_hdrc_imx_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(ci_hdrc_imx_suspend, ci_hdrc_imx_resume) + SET_RUNTIME_PM_OPS(ci_hdrc_imx_runtime_suspend, + ci_hdrc_imx_runtime_resume, NULL) +}; +static struct platform_driver ci_hdrc_imx_driver = { + .probe = ci_hdrc_imx_probe, + .remove = ci_hdrc_imx_remove, + .shutdown = ci_hdrc_imx_shutdown, + .driver = { + .name = "imx_usb", + .of_match_table = ci_hdrc_imx_dt_ids, + .pm = &ci_hdrc_imx_pm_ops, + }, +}; + +module_platform_driver(ci_hdrc_imx_driver); + +MODULE_ALIAS("platform:imx-usb"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("CI HDRC i.MX USB binding"); +MODULE_AUTHOR("Marek Vasut <marex@denx.de>"); +MODULE_AUTHOR("Richard Zhao <richard.zhao@freescale.com>"); diff --git a/drivers/usb/chipidea/ci_hdrc_imx.h b/drivers/usb/chipidea/ci_hdrc_imx.h new file mode 100644 index 000000000..204275f47 --- /dev/null +++ b/drivers/usb/chipidea/ci_hdrc_imx.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2012 Freescale Semiconductor, Inc. + */ + +#ifndef __DRIVER_USB_CHIPIDEA_CI_HDRC_IMX_H +#define __DRIVER_USB_CHIPIDEA_CI_HDRC_IMX_H + +struct imx_usbmisc_data { + struct device *dev; + int index; + + unsigned int disable_oc:1; /* over current detect disabled */ + unsigned int oc_polarity:1; /* over current polarity if oc enabled */ + unsigned int evdo:1; /* set external vbus divider option */ + unsigned int ulpi:1; /* connected to an ULPI phy */ +}; + +int imx_usbmisc_init(struct imx_usbmisc_data *); +int imx_usbmisc_init_post(struct imx_usbmisc_data *); +int imx_usbmisc_set_wakeup(struct imx_usbmisc_data *, bool); + +#endif /* __DRIVER_USB_CHIPIDEA_CI_HDRC_IMX_H */ diff --git a/drivers/usb/chipidea/ci_hdrc_msm.c b/drivers/usb/chipidea/ci_hdrc_msm.c new file mode 100644 index 000000000..880009987 --- /dev/null +++ b/drivers/usb/chipidea/ci_hdrc_msm.c @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2010, Code Aurora Forum. All rights reserved. */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/usb/chipidea.h> +#include <linux/clk.h> +#include <linux/reset.h> +#include <linux/mfd/syscon.h> +#include <linux/regmap.h> +#include <linux/io.h> +#include <linux/reset-controller.h> +#include <linux/extcon.h> +#include <linux/of.h> + +#include "ci.h" + +#define HS_PHY_AHB_MODE 0x0098 + +#define HS_PHY_GENCONFIG 0x009c +#define HS_PHY_TXFIFO_IDLE_FORCE_DIS BIT(4) + +#define HS_PHY_GENCONFIG_2 0x00a0 +#define HS_PHY_SESS_VLD_CTRL_EN BIT(7) +#define HS_PHY_ULPI_TX_PKT_EN_CLR_FIX BIT(19) + +#define HSPHY_SESS_VLD_CTRL BIT(25) + +/* Vendor base starts at 0x200 beyond CI base */ +#define HS_PHY_CTRL 0x0040 +#define HS_PHY_SEC_CTRL 0x0078 +#define HS_PHY_DIG_CLAMP_N BIT(16) +#define HS_PHY_POR_ASSERT BIT(0) + +struct ci_hdrc_msm { + struct platform_device *ci; + struct clk *core_clk; + struct clk *iface_clk; + struct clk *fs_clk; + struct ci_hdrc_platform_data pdata; + struct reset_controller_dev rcdev; + bool secondary_phy; + bool hsic; + void __iomem *base; +}; + +static int +ci_hdrc_msm_por_reset(struct reset_controller_dev *r, unsigned long id) +{ + struct ci_hdrc_msm *ci_msm = container_of(r, struct ci_hdrc_msm, rcdev); + void __iomem *addr = ci_msm->base; + u32 val; + + if (id) + addr += HS_PHY_SEC_CTRL; + else + addr += HS_PHY_CTRL; + + val = readl_relaxed(addr); + val |= HS_PHY_POR_ASSERT; + writel(val, addr); + /* + * wait for minimum 10 microseconds as suggested by manual. + * Use a slightly larger value since the exact value didn't + * work 100% of the time. + */ + udelay(12); + val &= ~HS_PHY_POR_ASSERT; + writel(val, addr); + + return 0; +} + +static const struct reset_control_ops ci_hdrc_msm_reset_ops = { + .reset = ci_hdrc_msm_por_reset, +}; + +static int ci_hdrc_msm_notify_event(struct ci_hdrc *ci, unsigned event) +{ + struct device *dev = ci->dev->parent; + struct ci_hdrc_msm *msm_ci = dev_get_drvdata(dev); + int ret; + + switch (event) { + case CI_HDRC_CONTROLLER_RESET_EVENT: + dev_dbg(dev, "CI_HDRC_CONTROLLER_RESET_EVENT received\n"); + + hw_phymode_configure(ci); + if (msm_ci->secondary_phy) { + u32 val = readl_relaxed(msm_ci->base + HS_PHY_SEC_CTRL); + val |= HS_PHY_DIG_CLAMP_N; + writel_relaxed(val, msm_ci->base + HS_PHY_SEC_CTRL); + } + + ret = phy_init(ci->phy); + if (ret) + return ret; + + ret = phy_power_on(ci->phy); + if (ret) { + phy_exit(ci->phy); + return ret; + } + + /* use AHB transactor, allow posted data writes */ + hw_write_id_reg(ci, HS_PHY_AHB_MODE, 0xffffffff, 0x8); + + /* workaround for rx buffer collision issue */ + hw_write_id_reg(ci, HS_PHY_GENCONFIG, + HS_PHY_TXFIFO_IDLE_FORCE_DIS, 0); + + if (!msm_ci->hsic) + hw_write_id_reg(ci, HS_PHY_GENCONFIG_2, + HS_PHY_ULPI_TX_PKT_EN_CLR_FIX, 0); + + if (!IS_ERR(ci->platdata->vbus_extcon.edev)) { + hw_write_id_reg(ci, HS_PHY_GENCONFIG_2, + HS_PHY_SESS_VLD_CTRL_EN, + HS_PHY_SESS_VLD_CTRL_EN); + hw_write(ci, OP_USBCMD, HSPHY_SESS_VLD_CTRL, + HSPHY_SESS_VLD_CTRL); + + } + break; + case CI_HDRC_CONTROLLER_STOPPED_EVENT: + dev_dbg(dev, "CI_HDRC_CONTROLLER_STOPPED_EVENT received\n"); + phy_power_off(ci->phy); + phy_exit(ci->phy); + break; + default: + dev_dbg(dev, "unknown ci_hdrc event\n"); + break; + } + + return 0; +} + +static int ci_hdrc_msm_mux_phy(struct ci_hdrc_msm *ci, + struct platform_device *pdev) +{ + struct regmap *regmap; + struct device *dev = &pdev->dev; + struct of_phandle_args args; + u32 val; + int ret; + + ret = of_parse_phandle_with_fixed_args(dev->of_node, "phy-select", 2, 0, + &args); + if (ret) + return 0; + + regmap = syscon_node_to_regmap(args.np); + of_node_put(args.np); + if (IS_ERR(regmap)) + return PTR_ERR(regmap); + + ret = regmap_write(regmap, args.args[0], args.args[1]); + if (ret) + return ret; + + ci->secondary_phy = !!args.args[1]; + if (ci->secondary_phy) { + val = readl_relaxed(ci->base + HS_PHY_SEC_CTRL); + val |= HS_PHY_DIG_CLAMP_N; + writel_relaxed(val, ci->base + HS_PHY_SEC_CTRL); + } + + return 0; +} + +static int ci_hdrc_msm_probe(struct platform_device *pdev) +{ + struct ci_hdrc_msm *ci; + struct platform_device *plat_ci; + struct clk *clk; + struct reset_control *reset; + struct resource *res; + int ret; + struct device_node *ulpi_node, *phy_node; + + dev_dbg(&pdev->dev, "ci_hdrc_msm_probe\n"); + + ci = devm_kzalloc(&pdev->dev, sizeof(*ci), GFP_KERNEL); + if (!ci) + return -ENOMEM; + platform_set_drvdata(pdev, ci); + + ci->pdata.name = "ci_hdrc_msm"; + ci->pdata.capoffset = DEF_CAPOFFSET; + ci->pdata.flags = CI_HDRC_REGS_SHARED | CI_HDRC_DISABLE_STREAMING | + CI_HDRC_OVERRIDE_AHB_BURST | + CI_HDRC_OVERRIDE_PHY_CONTROL; + ci->pdata.notify_event = ci_hdrc_msm_notify_event; + + reset = devm_reset_control_get(&pdev->dev, "core"); + if (IS_ERR(reset)) + return PTR_ERR(reset); + + ci->core_clk = clk = devm_clk_get(&pdev->dev, "core"); + if (IS_ERR(clk)) + return PTR_ERR(clk); + + ci->iface_clk = clk = devm_clk_get(&pdev->dev, "iface"); + if (IS_ERR(clk)) + return PTR_ERR(clk); + + ci->fs_clk = clk = devm_clk_get(&pdev->dev, "fs"); + if (IS_ERR(clk)) { + if (PTR_ERR(clk) == -EPROBE_DEFER) + return -EPROBE_DEFER; + ci->fs_clk = NULL; + } + + res = platform_get_resource(pdev, IORESOURCE_MEM, 1); + ci->base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(ci->base)) + return PTR_ERR(ci->base); + + ci->rcdev.owner = THIS_MODULE; + ci->rcdev.ops = &ci_hdrc_msm_reset_ops; + ci->rcdev.of_node = pdev->dev.of_node; + ci->rcdev.nr_resets = 2; + ret = reset_controller_register(&ci->rcdev); + if (ret) + return ret; + + ret = clk_prepare_enable(ci->fs_clk); + if (ret) + goto err_fs; + + reset_control_assert(reset); + usleep_range(10000, 12000); + reset_control_deassert(reset); + + clk_disable_unprepare(ci->fs_clk); + + ret = clk_prepare_enable(ci->core_clk); + if (ret) + goto err_fs; + + ret = clk_prepare_enable(ci->iface_clk); + if (ret) + goto err_iface; + + ret = ci_hdrc_msm_mux_phy(ci, pdev); + if (ret) + goto err_mux; + + ulpi_node = of_get_child_by_name(pdev->dev.of_node, "ulpi"); + if (ulpi_node) { + phy_node = of_get_next_available_child(ulpi_node, NULL); + ci->hsic = of_device_is_compatible(phy_node, "qcom,usb-hsic-phy"); + of_node_put(phy_node); + } + of_node_put(ulpi_node); + + plat_ci = ci_hdrc_add_device(&pdev->dev, pdev->resource, + pdev->num_resources, &ci->pdata); + if (IS_ERR(plat_ci)) { + ret = PTR_ERR(plat_ci); + if (ret != -EPROBE_DEFER) + dev_err(&pdev->dev, "ci_hdrc_add_device failed!\n"); + goto err_mux; + } + + ci->ci = plat_ci; + + pm_runtime_set_active(&pdev->dev); + pm_runtime_no_callbacks(&pdev->dev); + pm_runtime_enable(&pdev->dev); + + return 0; + +err_mux: + clk_disable_unprepare(ci->iface_clk); +err_iface: + clk_disable_unprepare(ci->core_clk); +err_fs: + reset_controller_unregister(&ci->rcdev); + return ret; +} + +static int ci_hdrc_msm_remove(struct platform_device *pdev) +{ + struct ci_hdrc_msm *ci = platform_get_drvdata(pdev); + + pm_runtime_disable(&pdev->dev); + ci_hdrc_remove_device(ci->ci); + clk_disable_unprepare(ci->iface_clk); + clk_disable_unprepare(ci->core_clk); + reset_controller_unregister(&ci->rcdev); + + return 0; +} + +static const struct of_device_id msm_ci_dt_match[] = { + { .compatible = "qcom,ci-hdrc", }, + { } +}; +MODULE_DEVICE_TABLE(of, msm_ci_dt_match); + +static struct platform_driver ci_hdrc_msm_driver = { + .probe = ci_hdrc_msm_probe, + .remove = ci_hdrc_msm_remove, + .driver = { + .name = "msm_hsusb", + .of_match_table = msm_ci_dt_match, + }, +}; + +module_platform_driver(ci_hdrc_msm_driver); + +MODULE_ALIAS("platform:msm_hsusb"); +MODULE_ALIAS("platform:ci13xxx_msm"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/chipidea/ci_hdrc_pci.c b/drivers/usb/chipidea/ci_hdrc_pci.c new file mode 100644 index 000000000..49a61549c --- /dev/null +++ b/drivers/usb/chipidea/ci_hdrc_pci.c @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ci_hdrc_pci.c - MIPS USB IP core family device controller + * + * Copyright (C) 2008 Chipidea - MIPS Technologies, Inc. All rights reserved. + * + * Author: David Lopo + */ + +#include <linux/platform_device.h> +#include <linux/module.h> +#include <linux/pci.h> +#include <linux/interrupt.h> +#include <linux/usb/gadget.h> +#include <linux/usb/chipidea.h> +#include <linux/usb/usb_phy_generic.h> + +/* driver name */ +#define UDC_DRIVER_NAME "ci_hdrc_pci" + +struct ci_hdrc_pci { + struct platform_device *ci; + struct platform_device *phy; +}; + +/****************************************************************************** + * PCI block + *****************************************************************************/ +static struct ci_hdrc_platform_data pci_platdata = { + .name = UDC_DRIVER_NAME, + .capoffset = DEF_CAPOFFSET, +}; + +static struct ci_hdrc_platform_data langwell_pci_platdata = { + .name = UDC_DRIVER_NAME, + .capoffset = 0, +}; + +static struct ci_hdrc_platform_data penwell_pci_platdata = { + .name = UDC_DRIVER_NAME, + .capoffset = 0, + .power_budget = 200, +}; + +/** + * ci_hdrc_pci_probe: PCI probe + * @pdev: USB device controller being probed + * @id: PCI hotplug ID connecting controller to UDC framework + * + * This function returns an error code + * Allocates basic PCI resources for this USB device controller, and then + * invokes the udc_probe() method to start the UDC associated with it + */ +static int ci_hdrc_pci_probe(struct pci_dev *pdev, + const struct pci_device_id *id) +{ + struct ci_hdrc_platform_data *platdata = (void *)id->driver_data; + struct ci_hdrc_pci *ci; + struct resource res[3]; + int retval = 0, nres = 2; + + if (!platdata) { + dev_err(&pdev->dev, "device doesn't provide driver data\n"); + return -ENODEV; + } + + ci = devm_kzalloc(&pdev->dev, sizeof(*ci), GFP_KERNEL); + if (!ci) + return -ENOMEM; + + retval = pcim_enable_device(pdev); + if (retval) + return retval; + + if (!pdev->irq) { + dev_err(&pdev->dev, "No IRQ, check BIOS/PCI setup!"); + return -ENODEV; + } + + pci_set_master(pdev); + pci_try_set_mwi(pdev); + + /* register a nop PHY */ + ci->phy = usb_phy_generic_register(); + if (IS_ERR(ci->phy)) + return PTR_ERR(ci->phy); + + memset(res, 0, sizeof(res)); + res[0].start = pci_resource_start(pdev, 0); + res[0].end = pci_resource_end(pdev, 0); + res[0].flags = IORESOURCE_MEM; + res[1].start = pdev->irq; + res[1].flags = IORESOURCE_IRQ; + + ci->ci = ci_hdrc_add_device(&pdev->dev, res, nres, platdata); + if (IS_ERR(ci->ci)) { + dev_err(&pdev->dev, "ci_hdrc_add_device failed!\n"); + usb_phy_generic_unregister(ci->phy); + return PTR_ERR(ci->ci); + } + + pci_set_drvdata(pdev, ci); + + return 0; +} + +/** + * ci_hdrc_pci_remove: PCI remove + * @pdev: USB Device Controller being removed + * + * Reverses the effect of ci_hdrc_pci_probe(), + * first invoking the udc_remove() and then releases + * all PCI resources allocated for this USB device controller + */ +static void ci_hdrc_pci_remove(struct pci_dev *pdev) +{ + struct ci_hdrc_pci *ci = pci_get_drvdata(pdev); + + ci_hdrc_remove_device(ci->ci); + usb_phy_generic_unregister(ci->phy); +} + +/** + * PCI device table + * PCI device structure + * + * Check "pci.h" for details + * + * Note: ehci-pci driver may try to probe the device first. You have to add an + * ID to the bypass_pci_id_table in ehci-pci driver to prevent this. + */ +static const struct pci_device_id ci_hdrc_pci_id_table[] = { + { + PCI_DEVICE(0x153F, 0x1004), + .driver_data = (kernel_ulong_t)&pci_platdata, + }, + { + PCI_DEVICE(0x153F, 0x1006), + .driver_data = (kernel_ulong_t)&pci_platdata, + }, + { + PCI_VDEVICE(INTEL, 0x0811), + .driver_data = (kernel_ulong_t)&langwell_pci_platdata, + }, + { + PCI_VDEVICE(INTEL, 0x0829), + .driver_data = (kernel_ulong_t)&penwell_pci_platdata, + }, + { + /* Intel Clovertrail */ + PCI_VDEVICE(INTEL, 0xe006), + .driver_data = (kernel_ulong_t)&penwell_pci_platdata, + }, + { 0 } /* end: all zeroes */ +}; +MODULE_DEVICE_TABLE(pci, ci_hdrc_pci_id_table); + +static struct pci_driver ci_hdrc_pci_driver = { + .name = UDC_DRIVER_NAME, + .id_table = ci_hdrc_pci_id_table, + .probe = ci_hdrc_pci_probe, + .remove = ci_hdrc_pci_remove, +}; + +module_pci_driver(ci_hdrc_pci_driver); + +MODULE_AUTHOR("MIPS - David Lopo <dlopo@chipidea.mips.com>"); +MODULE_DESCRIPTION("MIPS CI13XXX USB Peripheral Controller"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:ci13xxx_pci"); diff --git a/drivers/usb/chipidea/ci_hdrc_tegra.c b/drivers/usb/chipidea/ci_hdrc_tegra.c new file mode 100644 index 000000000..12025358b --- /dev/null +++ b/drivers/usb/chipidea/ci_hdrc_tegra.c @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2016, NVIDIA Corporation + */ + +#include <linux/clk.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/reset.h> + +#include <linux/usb/chipidea.h> + +#include "ci.h" + +struct tegra_udc { + struct ci_hdrc_platform_data data; + struct platform_device *dev; + + struct usb_phy *phy; + struct clk *clk; +}; + +struct tegra_udc_soc_info { + unsigned long flags; +}; + +static const struct tegra_udc_soc_info tegra20_udc_soc_info = { + .flags = CI_HDRC_REQUIRES_ALIGNED_DMA, +}; + +static const struct tegra_udc_soc_info tegra30_udc_soc_info = { + .flags = CI_HDRC_REQUIRES_ALIGNED_DMA, +}; + +static const struct tegra_udc_soc_info tegra114_udc_soc_info = { + .flags = CI_HDRC_REQUIRES_ALIGNED_DMA, +}; + +static const struct tegra_udc_soc_info tegra124_udc_soc_info = { + .flags = CI_HDRC_REQUIRES_ALIGNED_DMA, +}; + +static const struct of_device_id tegra_udc_of_match[] = { + { + .compatible = "nvidia,tegra20-udc", + .data = &tegra20_udc_soc_info, + }, { + .compatible = "nvidia,tegra30-udc", + .data = &tegra30_udc_soc_info, + }, { + .compatible = "nvidia,tegra114-udc", + .data = &tegra114_udc_soc_info, + }, { + .compatible = "nvidia,tegra124-udc", + .data = &tegra124_udc_soc_info, + }, { + /* sentinel */ + } +}; +MODULE_DEVICE_TABLE(of, tegra_udc_of_match); + +static int tegra_udc_probe(struct platform_device *pdev) +{ + const struct tegra_udc_soc_info *soc; + struct tegra_udc *udc; + int err; + + udc = devm_kzalloc(&pdev->dev, sizeof(*udc), GFP_KERNEL); + if (!udc) + return -ENOMEM; + + soc = of_device_get_match_data(&pdev->dev); + if (!soc) { + dev_err(&pdev->dev, "failed to match OF data\n"); + return -EINVAL; + } + + udc->phy = devm_usb_get_phy_by_phandle(&pdev->dev, "nvidia,phy", 0); + if (IS_ERR(udc->phy)) { + err = PTR_ERR(udc->phy); + dev_err(&pdev->dev, "failed to get PHY: %d\n", err); + return err; + } + + udc->clk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(udc->clk)) { + err = PTR_ERR(udc->clk); + dev_err(&pdev->dev, "failed to get clock: %d\n", err); + return err; + } + + err = clk_prepare_enable(udc->clk); + if (err < 0) { + dev_err(&pdev->dev, "failed to enable clock: %d\n", err); + return err; + } + + /* + * Tegra's USB PHY driver doesn't implement optional phy_init() + * hook, so we have to power on UDC controller before ChipIdea + * driver initialization kicks in. + */ + usb_phy_set_suspend(udc->phy, 0); + + /* setup and register ChipIdea HDRC device */ + udc->data.name = "tegra-udc"; + udc->data.flags = soc->flags; + udc->data.usb_phy = udc->phy; + udc->data.capoffset = DEF_CAPOFFSET; + + udc->dev = ci_hdrc_add_device(&pdev->dev, pdev->resource, + pdev->num_resources, &udc->data); + if (IS_ERR(udc->dev)) { + err = PTR_ERR(udc->dev); + dev_err(&pdev->dev, "failed to add HDRC device: %d\n", err); + goto fail_power_off; + } + + platform_set_drvdata(pdev, udc); + + return 0; + +fail_power_off: + usb_phy_set_suspend(udc->phy, 1); + clk_disable_unprepare(udc->clk); + return err; +} + +static int tegra_udc_remove(struct platform_device *pdev) +{ + struct tegra_udc *udc = platform_get_drvdata(pdev); + + ci_hdrc_remove_device(udc->dev); + usb_phy_set_suspend(udc->phy, 1); + clk_disable_unprepare(udc->clk); + + return 0; +} + +static struct platform_driver tegra_udc_driver = { + .driver = { + .name = "tegra-udc", + .of_match_table = tegra_udc_of_match, + }, + .probe = tegra_udc_probe, + .remove = tegra_udc_remove, +}; +module_platform_driver(tegra_udc_driver); + +MODULE_DESCRIPTION("NVIDIA Tegra USB device mode driver"); +MODULE_AUTHOR("Thierry Reding <treding@nvidia.com>"); +MODULE_ALIAS("platform:tegra-udc"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/chipidea/ci_hdrc_usb2.c b/drivers/usb/chipidea/ci_hdrc_usb2.c new file mode 100644 index 000000000..c044fba46 --- /dev/null +++ b/drivers/usb/chipidea/ci_hdrc_usb2.c @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2014 Marvell Technology Group Ltd. + * + * Antoine Tenart <antoine.tenart@free-electrons.com> + */ + +#include <linux/clk.h> +#include <linux/dma-mapping.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/phy/phy.h> +#include <linux/platform_device.h> +#include <linux/usb/chipidea.h> +#include <linux/usb/hcd.h> +#include <linux/usb/ulpi.h> + +#include "ci.h" + +struct ci_hdrc_usb2_priv { + struct platform_device *ci_pdev; + struct clk *clk; +}; + +static const struct ci_hdrc_platform_data ci_default_pdata = { + .capoffset = DEF_CAPOFFSET, + .flags = CI_HDRC_DISABLE_STREAMING, +}; + +static struct ci_hdrc_platform_data ci_zynq_pdata = { + .capoffset = DEF_CAPOFFSET, +}; + +static const struct of_device_id ci_hdrc_usb2_of_match[] = { + { .compatible = "chipidea,usb2"}, + { .compatible = "xlnx,zynq-usb-2.20a", .data = &ci_zynq_pdata}, + { } +}; +MODULE_DEVICE_TABLE(of, ci_hdrc_usb2_of_match); + +static int ci_hdrc_usb2_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct ci_hdrc_usb2_priv *priv; + struct ci_hdrc_platform_data *ci_pdata = dev_get_platdata(dev); + int ret; + const struct of_device_id *match; + + if (!ci_pdata) { + ci_pdata = devm_kmalloc(dev, sizeof(*ci_pdata), GFP_KERNEL); + if (!ci_pdata) + return -ENOMEM; + *ci_pdata = ci_default_pdata; /* struct copy */ + } + + match = of_match_device(ci_hdrc_usb2_of_match, &pdev->dev); + if (match && match->data) { + /* struct copy */ + *ci_pdata = *(struct ci_hdrc_platform_data *)match->data; + } + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->clk = devm_clk_get(dev, NULL); + if (!IS_ERR(priv->clk)) { + ret = clk_prepare_enable(priv->clk); + if (ret) { + dev_err(dev, "failed to enable the clock: %d\n", ret); + return ret; + } + } + + ci_pdata->name = dev_name(dev); + + priv->ci_pdev = ci_hdrc_add_device(dev, pdev->resource, + pdev->num_resources, ci_pdata); + if (IS_ERR(priv->ci_pdev)) { + ret = PTR_ERR(priv->ci_pdev); + if (ret != -EPROBE_DEFER) + dev_err(dev, + "failed to register ci_hdrc platform device: %d\n", + ret); + goto clk_err; + } + + platform_set_drvdata(pdev, priv); + + pm_runtime_no_callbacks(dev); + pm_runtime_enable(dev); + + return 0; + +clk_err: + if (!IS_ERR(priv->clk)) + clk_disable_unprepare(priv->clk); + return ret; +} + +static int ci_hdrc_usb2_remove(struct platform_device *pdev) +{ + struct ci_hdrc_usb2_priv *priv = platform_get_drvdata(pdev); + + pm_runtime_disable(&pdev->dev); + ci_hdrc_remove_device(priv->ci_pdev); + clk_disable_unprepare(priv->clk); + + return 0; +} + +static struct platform_driver ci_hdrc_usb2_driver = { + .probe = ci_hdrc_usb2_probe, + .remove = ci_hdrc_usb2_remove, + .driver = { + .name = "chipidea-usb2", + .of_match_table = of_match_ptr(ci_hdrc_usb2_of_match), + }, +}; +module_platform_driver(ci_hdrc_usb2_driver); + +MODULE_DESCRIPTION("ChipIdea HDRC USB2 binding for ci13xxx"); +MODULE_AUTHOR("Antoine Tenart <antoine.tenart@free-electrons.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/chipidea/ci_hdrc_zevio.c b/drivers/usb/chipidea/ci_hdrc_zevio.c new file mode 100644 index 000000000..e1634da4a --- /dev/null +++ b/drivers/usb/chipidea/ci_hdrc_zevio.c @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2013 Daniel Tang <tangrs@tangrs.id.au> + * + * Based off drivers/usb/chipidea/ci_hdrc_msm.c + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/usb/gadget.h> +#include <linux/usb/chipidea.h> + +#include "ci.h" + +static struct ci_hdrc_platform_data ci_hdrc_zevio_platdata = { + .name = "ci_hdrc_zevio", + .flags = CI_HDRC_REGS_SHARED | CI_HDRC_FORCE_FULLSPEED, + .capoffset = DEF_CAPOFFSET, +}; + +static int ci_hdrc_zevio_probe(struct platform_device *pdev) +{ + struct platform_device *ci_pdev; + + dev_dbg(&pdev->dev, "ci_hdrc_zevio_probe\n"); + + ci_pdev = ci_hdrc_add_device(&pdev->dev, + pdev->resource, pdev->num_resources, + &ci_hdrc_zevio_platdata); + + if (IS_ERR(ci_pdev)) { + dev_err(&pdev->dev, "ci_hdrc_add_device failed!\n"); + return PTR_ERR(ci_pdev); + } + + platform_set_drvdata(pdev, ci_pdev); + + return 0; +} + +static int ci_hdrc_zevio_remove(struct platform_device *pdev) +{ + struct platform_device *ci_pdev = platform_get_drvdata(pdev); + + ci_hdrc_remove_device(ci_pdev); + + return 0; +} + +static const struct of_device_id ci_hdrc_zevio_dt_ids[] = { + { .compatible = "lsi,zevio-usb", }, + { /* sentinel */ } +}; + +static struct platform_driver ci_hdrc_zevio_driver = { + .probe = ci_hdrc_zevio_probe, + .remove = ci_hdrc_zevio_remove, + .driver = { + .name = "zevio_usb", + .of_match_table = ci_hdrc_zevio_dt_ids, + }, +}; + +MODULE_DEVICE_TABLE(of, ci_hdrc_zevio_dt_ids); +module_platform_driver(ci_hdrc_zevio_driver); + +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/chipidea/core.c b/drivers/usb/chipidea/core.c new file mode 100644 index 000000000..c13f9a153 --- /dev/null +++ b/drivers/usb/chipidea/core.c @@ -0,0 +1,1339 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * core.c - ChipIdea USB IP core family device controller + * + * Copyright (C) 2008 Chipidea - MIPS Technologies, Inc. All rights reserved. + * + * Author: David Lopo + */ + +/* + * Description: ChipIdea USB IP core family device controller + * + * This driver is composed of several blocks: + * - HW: hardware interface + * - DBG: debug facilities (optional) + * - UTIL: utilities + * - ISR: interrupts handling + * - ENDPT: endpoint operations (Gadget API) + * - GADGET: gadget operations (Gadget API) + * - BUS: bus glue code, bus abstraction layer + * + * Compile Options + * - STALL_IN: non-empty bulk-in pipes cannot be halted + * if defined mass storage compliance succeeds but with warnings + * => case 4: Hi > Dn + * => case 5: Hi > Di + * => case 8: Hi <> Do + * if undefined usbtest 13 fails + * - TRACE: enable function tracing (depends on DEBUG) + * + * Main Features + * - Chapter 9 & Mass Storage Compliance with Gadget File Storage + * - Chapter 9 Compliance with Gadget Zero (STALL_IN undefined) + * - Normal & LPM support + * + * USBTEST Report + * - OK: 0-12, 13 (STALL_IN defined) & 14 + * - Not Supported: 15 & 16 (ISO) + * + * TODO List + * - Suspend & Remote Wakeup + */ +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/dma-mapping.h> +#include <linux/extcon.h> +#include <linux/phy/phy.h> +#include <linux/platform_device.h> +#include <linux/module.h> +#include <linux/idr.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/pm_runtime.h> +#include <linux/usb/ch9.h> +#include <linux/usb/gadget.h> +#include <linux/usb/otg.h> +#include <linux/usb/chipidea.h> +#include <linux/usb/of.h> +#include <linux/of.h> +#include <linux/regulator/consumer.h> +#include <linux/usb/ehci_def.h> + +#include "ci.h" +#include "udc.h" +#include "bits.h" +#include "host.h" +#include "otg.h" +#include "otg_fsm.h" + +/* Controller register map */ +static const u8 ci_regs_nolpm[] = { + [CAP_CAPLENGTH] = 0x00U, + [CAP_HCCPARAMS] = 0x08U, + [CAP_DCCPARAMS] = 0x24U, + [CAP_TESTMODE] = 0x38U, + [OP_USBCMD] = 0x00U, + [OP_USBSTS] = 0x04U, + [OP_USBINTR] = 0x08U, + [OP_DEVICEADDR] = 0x14U, + [OP_ENDPTLISTADDR] = 0x18U, + [OP_TTCTRL] = 0x1CU, + [OP_BURSTSIZE] = 0x20U, + [OP_ULPI_VIEWPORT] = 0x30U, + [OP_PORTSC] = 0x44U, + [OP_DEVLC] = 0x84U, + [OP_OTGSC] = 0x64U, + [OP_USBMODE] = 0x68U, + [OP_ENDPTSETUPSTAT] = 0x6CU, + [OP_ENDPTPRIME] = 0x70U, + [OP_ENDPTFLUSH] = 0x74U, + [OP_ENDPTSTAT] = 0x78U, + [OP_ENDPTCOMPLETE] = 0x7CU, + [OP_ENDPTCTRL] = 0x80U, +}; + +static const u8 ci_regs_lpm[] = { + [CAP_CAPLENGTH] = 0x00U, + [CAP_HCCPARAMS] = 0x08U, + [CAP_DCCPARAMS] = 0x24U, + [CAP_TESTMODE] = 0xFCU, + [OP_USBCMD] = 0x00U, + [OP_USBSTS] = 0x04U, + [OP_USBINTR] = 0x08U, + [OP_DEVICEADDR] = 0x14U, + [OP_ENDPTLISTADDR] = 0x18U, + [OP_TTCTRL] = 0x1CU, + [OP_BURSTSIZE] = 0x20U, + [OP_ULPI_VIEWPORT] = 0x30U, + [OP_PORTSC] = 0x44U, + [OP_DEVLC] = 0x84U, + [OP_OTGSC] = 0xC4U, + [OP_USBMODE] = 0xC8U, + [OP_ENDPTSETUPSTAT] = 0xD8U, + [OP_ENDPTPRIME] = 0xDCU, + [OP_ENDPTFLUSH] = 0xE0U, + [OP_ENDPTSTAT] = 0xE4U, + [OP_ENDPTCOMPLETE] = 0xE8U, + [OP_ENDPTCTRL] = 0xECU, +}; + +static void hw_alloc_regmap(struct ci_hdrc *ci, bool is_lpm) +{ + int i; + + for (i = 0; i < OP_ENDPTCTRL; i++) + ci->hw_bank.regmap[i] = + (i <= CAP_LAST ? ci->hw_bank.cap : ci->hw_bank.op) + + (is_lpm ? ci_regs_lpm[i] : ci_regs_nolpm[i]); + + for (; i <= OP_LAST; i++) + ci->hw_bank.regmap[i] = ci->hw_bank.op + + 4 * (i - OP_ENDPTCTRL) + + (is_lpm + ? ci_regs_lpm[OP_ENDPTCTRL] + : ci_regs_nolpm[OP_ENDPTCTRL]); + +} + +static enum ci_revision ci_get_revision(struct ci_hdrc *ci) +{ + int ver = hw_read_id_reg(ci, ID_ID, VERSION) >> __ffs(VERSION); + enum ci_revision rev = CI_REVISION_UNKNOWN; + + if (ver == 0x2) { + rev = hw_read_id_reg(ci, ID_ID, REVISION) + >> __ffs(REVISION); + rev += CI_REVISION_20; + } else if (ver == 0x0) { + rev = CI_REVISION_1X; + } + + return rev; +} + +/** + * hw_read_intr_enable: returns interrupt enable register + * + * @ci: the controller + * + * This function returns register data + */ +u32 hw_read_intr_enable(struct ci_hdrc *ci) +{ + return hw_read(ci, OP_USBINTR, ~0); +} + +/** + * hw_read_intr_status: returns interrupt status register + * + * @ci: the controller + * + * This function returns register data + */ +u32 hw_read_intr_status(struct ci_hdrc *ci) +{ + return hw_read(ci, OP_USBSTS, ~0); +} + +/** + * hw_port_test_set: writes port test mode (execute without interruption) + * @mode: new value + * + * This function returns an error code + */ +int hw_port_test_set(struct ci_hdrc *ci, u8 mode) +{ + const u8 TEST_MODE_MAX = 7; + + if (mode > TEST_MODE_MAX) + return -EINVAL; + + hw_write(ci, OP_PORTSC, PORTSC_PTC, mode << __ffs(PORTSC_PTC)); + return 0; +} + +/** + * hw_port_test_get: reads port test mode value + * + * @ci: the controller + * + * This function returns port test mode value + */ +u8 hw_port_test_get(struct ci_hdrc *ci) +{ + return hw_read(ci, OP_PORTSC, PORTSC_PTC) >> __ffs(PORTSC_PTC); +} + +static void hw_wait_phy_stable(void) +{ + /* + * The phy needs some delay to output the stable status from low + * power mode. And for OTGSC, the status inputs are debounced + * using a 1 ms time constant, so, delay 2ms for controller to get + * the stable status, like vbus and id when the phy leaves low power. + */ + usleep_range(2000, 2500); +} + +/* The PHY enters/leaves low power mode */ +static void ci_hdrc_enter_lpm(struct ci_hdrc *ci, bool enable) +{ + enum ci_hw_regs reg = ci->hw_bank.lpm ? OP_DEVLC : OP_PORTSC; + bool lpm = !!(hw_read(ci, reg, PORTSC_PHCD(ci->hw_bank.lpm))); + + if (enable && !lpm) + hw_write(ci, reg, PORTSC_PHCD(ci->hw_bank.lpm), + PORTSC_PHCD(ci->hw_bank.lpm)); + else if (!enable && lpm) + hw_write(ci, reg, PORTSC_PHCD(ci->hw_bank.lpm), + 0); +} + +static int hw_device_init(struct ci_hdrc *ci, void __iomem *base) +{ + u32 reg; + + /* bank is a module variable */ + ci->hw_bank.abs = base; + + ci->hw_bank.cap = ci->hw_bank.abs; + ci->hw_bank.cap += ci->platdata->capoffset; + ci->hw_bank.op = ci->hw_bank.cap + (ioread32(ci->hw_bank.cap) & 0xff); + + hw_alloc_regmap(ci, false); + reg = hw_read(ci, CAP_HCCPARAMS, HCCPARAMS_LEN) >> + __ffs(HCCPARAMS_LEN); + ci->hw_bank.lpm = reg; + if (reg) + hw_alloc_regmap(ci, !!reg); + ci->hw_bank.size = ci->hw_bank.op - ci->hw_bank.abs; + ci->hw_bank.size += OP_LAST; + ci->hw_bank.size /= sizeof(u32); + + reg = hw_read(ci, CAP_DCCPARAMS, DCCPARAMS_DEN) >> + __ffs(DCCPARAMS_DEN); + ci->hw_ep_max = reg * 2; /* cache hw ENDPT_MAX */ + + if (ci->hw_ep_max > ENDPT_MAX) + return -ENODEV; + + ci_hdrc_enter_lpm(ci, false); + + /* Disable all interrupts bits */ + hw_write(ci, OP_USBINTR, 0xffffffff, 0); + + /* Clear all interrupts status bits*/ + hw_write(ci, OP_USBSTS, 0xffffffff, 0xffffffff); + + ci->rev = ci_get_revision(ci); + + dev_dbg(ci->dev, + "ChipIdea HDRC found, revision: %d, lpm: %d; cap: %p op: %p\n", + ci->rev, ci->hw_bank.lpm, ci->hw_bank.cap, ci->hw_bank.op); + + /* setup lock mode ? */ + + /* ENDPTSETUPSTAT is '0' by default */ + + /* HCSPARAMS.bf.ppc SHOULD BE zero for device */ + + return 0; +} + +void hw_phymode_configure(struct ci_hdrc *ci) +{ + u32 portsc, lpm, sts = 0; + + switch (ci->platdata->phy_mode) { + case USBPHY_INTERFACE_MODE_UTMI: + portsc = PORTSC_PTS(PTS_UTMI); + lpm = DEVLC_PTS(PTS_UTMI); + break; + case USBPHY_INTERFACE_MODE_UTMIW: + portsc = PORTSC_PTS(PTS_UTMI) | PORTSC_PTW; + lpm = DEVLC_PTS(PTS_UTMI) | DEVLC_PTW; + break; + case USBPHY_INTERFACE_MODE_ULPI: + portsc = PORTSC_PTS(PTS_ULPI); + lpm = DEVLC_PTS(PTS_ULPI); + break; + case USBPHY_INTERFACE_MODE_SERIAL: + portsc = PORTSC_PTS(PTS_SERIAL); + lpm = DEVLC_PTS(PTS_SERIAL); + sts = 1; + break; + case USBPHY_INTERFACE_MODE_HSIC: + portsc = PORTSC_PTS(PTS_HSIC); + lpm = DEVLC_PTS(PTS_HSIC); + break; + default: + return; + } + + if (ci->hw_bank.lpm) { + hw_write(ci, OP_DEVLC, DEVLC_PTS(7) | DEVLC_PTW, lpm); + if (sts) + hw_write(ci, OP_DEVLC, DEVLC_STS, DEVLC_STS); + } else { + hw_write(ci, OP_PORTSC, PORTSC_PTS(7) | PORTSC_PTW, portsc); + if (sts) + hw_write(ci, OP_PORTSC, PORTSC_STS, PORTSC_STS); + } +} +EXPORT_SYMBOL_GPL(hw_phymode_configure); + +/** + * _ci_usb_phy_init: initialize phy taking in account both phy and usb_phy + * interfaces + * @ci: the controller + * + * This function returns an error code if the phy failed to init + */ +static int _ci_usb_phy_init(struct ci_hdrc *ci) +{ + int ret; + + if (ci->phy) { + ret = phy_init(ci->phy); + if (ret) + return ret; + + ret = phy_power_on(ci->phy); + if (ret) { + phy_exit(ci->phy); + return ret; + } + } else { + ret = usb_phy_init(ci->usb_phy); + } + + return ret; +} + +/** + * _ci_usb_phy_exit: deinitialize phy taking in account both phy and usb_phy + * interfaces + * @ci: the controller + */ +static void ci_usb_phy_exit(struct ci_hdrc *ci) +{ + if (ci->platdata->flags & CI_HDRC_OVERRIDE_PHY_CONTROL) + return; + + if (ci->phy) { + phy_power_off(ci->phy); + phy_exit(ci->phy); + } else { + usb_phy_shutdown(ci->usb_phy); + } +} + +/** + * ci_usb_phy_init: initialize phy according to different phy type + * @ci: the controller + * + * This function returns an error code if usb_phy_init has failed + */ +static int ci_usb_phy_init(struct ci_hdrc *ci) +{ + int ret; + + if (ci->platdata->flags & CI_HDRC_OVERRIDE_PHY_CONTROL) + return 0; + + switch (ci->platdata->phy_mode) { + case USBPHY_INTERFACE_MODE_UTMI: + case USBPHY_INTERFACE_MODE_UTMIW: + case USBPHY_INTERFACE_MODE_HSIC: + ret = _ci_usb_phy_init(ci); + if (!ret) + hw_wait_phy_stable(); + else + return ret; + hw_phymode_configure(ci); + break; + case USBPHY_INTERFACE_MODE_ULPI: + case USBPHY_INTERFACE_MODE_SERIAL: + hw_phymode_configure(ci); + ret = _ci_usb_phy_init(ci); + if (ret) + return ret; + break; + default: + ret = _ci_usb_phy_init(ci); + if (!ret) + hw_wait_phy_stable(); + } + + return ret; +} + + +/** + * ci_platform_configure: do controller configure + * @ci: the controller + * + */ +void ci_platform_configure(struct ci_hdrc *ci) +{ + bool is_device_mode, is_host_mode; + + is_device_mode = hw_read(ci, OP_USBMODE, USBMODE_CM) == USBMODE_CM_DC; + is_host_mode = hw_read(ci, OP_USBMODE, USBMODE_CM) == USBMODE_CM_HC; + + if (is_device_mode) { + phy_set_mode(ci->phy, PHY_MODE_USB_DEVICE); + + if (ci->platdata->flags & CI_HDRC_DISABLE_DEVICE_STREAMING) + hw_write(ci, OP_USBMODE, USBMODE_CI_SDIS, + USBMODE_CI_SDIS); + } + + if (is_host_mode) { + phy_set_mode(ci->phy, PHY_MODE_USB_HOST); + + if (ci->platdata->flags & CI_HDRC_DISABLE_HOST_STREAMING) + hw_write(ci, OP_USBMODE, USBMODE_CI_SDIS, + USBMODE_CI_SDIS); + } + + if (ci->platdata->flags & CI_HDRC_FORCE_FULLSPEED) { + if (ci->hw_bank.lpm) + hw_write(ci, OP_DEVLC, DEVLC_PFSC, DEVLC_PFSC); + else + hw_write(ci, OP_PORTSC, PORTSC_PFSC, PORTSC_PFSC); + } + + if (ci->platdata->flags & CI_HDRC_SET_NON_ZERO_TTHA) + hw_write(ci, OP_TTCTRL, TTCTRL_TTHA_MASK, TTCTRL_TTHA); + + hw_write(ci, OP_USBCMD, 0xff0000, ci->platdata->itc_setting << 16); + + if (ci->platdata->flags & CI_HDRC_OVERRIDE_AHB_BURST) + hw_write_id_reg(ci, ID_SBUSCFG, AHBBRST_MASK, + ci->platdata->ahb_burst_config); + + /* override burst size, take effect only when ahb_burst_config is 0 */ + if (!hw_read_id_reg(ci, ID_SBUSCFG, AHBBRST_MASK)) { + if (ci->platdata->flags & CI_HDRC_OVERRIDE_TX_BURST) + hw_write(ci, OP_BURSTSIZE, TX_BURST_MASK, + ci->platdata->tx_burst_size << __ffs(TX_BURST_MASK)); + + if (ci->platdata->flags & CI_HDRC_OVERRIDE_RX_BURST) + hw_write(ci, OP_BURSTSIZE, RX_BURST_MASK, + ci->platdata->rx_burst_size); + } +} + +/** + * hw_controller_reset: do controller reset + * @ci: the controller + * + * This function returns an error code + */ +static int hw_controller_reset(struct ci_hdrc *ci) +{ + int count = 0; + + hw_write(ci, OP_USBCMD, USBCMD_RST, USBCMD_RST); + while (hw_read(ci, OP_USBCMD, USBCMD_RST)) { + udelay(10); + if (count++ > 1000) + return -ETIMEDOUT; + } + + return 0; +} + +/** + * hw_device_reset: resets chip (execute without interruption) + * @ci: the controller + * + * This function returns an error code + */ +int hw_device_reset(struct ci_hdrc *ci) +{ + int ret; + + /* should flush & stop before reset */ + hw_write(ci, OP_ENDPTFLUSH, ~0, ~0); + hw_write(ci, OP_USBCMD, USBCMD_RS, 0); + + ret = hw_controller_reset(ci); + if (ret) { + dev_err(ci->dev, "error resetting controller, ret=%d\n", ret); + return ret; + } + + if (ci->platdata->notify_event) { + ret = ci->platdata->notify_event(ci, + CI_HDRC_CONTROLLER_RESET_EVENT); + if (ret) + return ret; + } + + /* USBMODE should be configured step by step */ + hw_write(ci, OP_USBMODE, USBMODE_CM, USBMODE_CM_IDLE); + hw_write(ci, OP_USBMODE, USBMODE_CM, USBMODE_CM_DC); + /* HW >= 2.3 */ + hw_write(ci, OP_USBMODE, USBMODE_SLOM, USBMODE_SLOM); + + if (hw_read(ci, OP_USBMODE, USBMODE_CM) != USBMODE_CM_DC) { + pr_err("cannot enter in %s device mode", ci_role(ci)->name); + pr_err("lpm = %i", ci->hw_bank.lpm); + return -ENODEV; + } + + ci_platform_configure(ci); + + return 0; +} + +static irqreturn_t ci_irq_handler(int irq, void *data) +{ + struct ci_hdrc *ci = data; + irqreturn_t ret = IRQ_NONE; + u32 otgsc = 0; + + if (ci->in_lpm) { + disable_irq_nosync(irq); + ci->wakeup_int = true; + pm_runtime_get(ci->dev); + return IRQ_HANDLED; + } + + if (ci->is_otg) { + otgsc = hw_read_otgsc(ci, ~0); + if (ci_otg_is_fsm_mode(ci)) { + ret = ci_otg_fsm_irq(ci); + if (ret == IRQ_HANDLED) + return ret; + } + } + + /* + * Handle id change interrupt, it indicates device/host function + * switch. + */ + if (ci->is_otg && (otgsc & OTGSC_IDIE) && (otgsc & OTGSC_IDIS)) { + ci->id_event = true; + /* Clear ID change irq status */ + hw_write_otgsc(ci, OTGSC_IDIS, OTGSC_IDIS); + ci_otg_queue_work(ci); + return IRQ_HANDLED; + } + + /* + * Handle vbus change interrupt, it indicates device connection + * and disconnection events. + */ + if (ci->is_otg && (otgsc & OTGSC_BSVIE) && (otgsc & OTGSC_BSVIS)) { + ci->b_sess_valid_event = true; + /* Clear BSV irq */ + hw_write_otgsc(ci, OTGSC_BSVIS, OTGSC_BSVIS); + ci_otg_queue_work(ci); + return IRQ_HANDLED; + } + + /* Handle device/host interrupt */ + if (ci->role != CI_ROLE_END) + ret = ci_role(ci)->irq(ci); + + return ret; +} + +static void ci_irq(struct ci_hdrc *ci) +{ + unsigned long flags; + + local_irq_save(flags); + ci_irq_handler(ci->irq, ci); + local_irq_restore(flags); +} + +static int ci_cable_notifier(struct notifier_block *nb, unsigned long event, + void *ptr) +{ + struct ci_hdrc_cable *cbl = container_of(nb, struct ci_hdrc_cable, nb); + struct ci_hdrc *ci = cbl->ci; + + cbl->connected = event; + cbl->changed = true; + + ci_irq(ci); + return NOTIFY_DONE; +} + +static int ci_get_platdata(struct device *dev, + struct ci_hdrc_platform_data *platdata) +{ + struct extcon_dev *ext_vbus, *ext_id; + struct ci_hdrc_cable *cable; + int ret; + + if (!platdata->phy_mode) + platdata->phy_mode = of_usb_get_phy_mode(dev->of_node); + + if (!platdata->dr_mode) + platdata->dr_mode = usb_get_dr_mode(dev); + + if (platdata->dr_mode == USB_DR_MODE_UNKNOWN) + platdata->dr_mode = USB_DR_MODE_OTG; + + if (platdata->dr_mode != USB_DR_MODE_PERIPHERAL) { + /* Get the vbus regulator */ + platdata->reg_vbus = devm_regulator_get(dev, "vbus"); + if (PTR_ERR(platdata->reg_vbus) == -EPROBE_DEFER) { + return -EPROBE_DEFER; + } else if (PTR_ERR(platdata->reg_vbus) == -ENODEV) { + /* no vbus regulator is needed */ + platdata->reg_vbus = NULL; + } else if (IS_ERR(platdata->reg_vbus)) { + dev_err(dev, "Getting regulator error: %ld\n", + PTR_ERR(platdata->reg_vbus)); + return PTR_ERR(platdata->reg_vbus); + } + /* Get TPL support */ + if (!platdata->tpl_support) + platdata->tpl_support = + of_usb_host_tpl_support(dev->of_node); + } + + if (platdata->dr_mode == USB_DR_MODE_OTG) { + /* We can support HNP and SRP of OTG 2.0 */ + platdata->ci_otg_caps.otg_rev = 0x0200; + platdata->ci_otg_caps.hnp_support = true; + platdata->ci_otg_caps.srp_support = true; + + /* Update otg capabilities by DT properties */ + ret = of_usb_update_otg_caps(dev->of_node, + &platdata->ci_otg_caps); + if (ret) + return ret; + } + + if (usb_get_maximum_speed(dev) == USB_SPEED_FULL) + platdata->flags |= CI_HDRC_FORCE_FULLSPEED; + + of_property_read_u32(dev->of_node, "phy-clkgate-delay-us", + &platdata->phy_clkgate_delay_us); + + platdata->itc_setting = 1; + + of_property_read_u32(dev->of_node, "itc-setting", + &platdata->itc_setting); + + ret = of_property_read_u32(dev->of_node, "ahb-burst-config", + &platdata->ahb_burst_config); + if (!ret) { + platdata->flags |= CI_HDRC_OVERRIDE_AHB_BURST; + } else if (ret != -EINVAL) { + dev_err(dev, "failed to get ahb-burst-config\n"); + return ret; + } + + ret = of_property_read_u32(dev->of_node, "tx-burst-size-dword", + &platdata->tx_burst_size); + if (!ret) { + platdata->flags |= CI_HDRC_OVERRIDE_TX_BURST; + } else if (ret != -EINVAL) { + dev_err(dev, "failed to get tx-burst-size-dword\n"); + return ret; + } + + ret = of_property_read_u32(dev->of_node, "rx-burst-size-dword", + &platdata->rx_burst_size); + if (!ret) { + platdata->flags |= CI_HDRC_OVERRIDE_RX_BURST; + } else if (ret != -EINVAL) { + dev_err(dev, "failed to get rx-burst-size-dword\n"); + return ret; + } + + if (of_find_property(dev->of_node, "non-zero-ttctrl-ttha", NULL)) + platdata->flags |= CI_HDRC_SET_NON_ZERO_TTHA; + + ext_id = ERR_PTR(-ENODEV); + ext_vbus = ERR_PTR(-ENODEV); + if (of_property_read_bool(dev->of_node, "extcon")) { + /* Each one of them is not mandatory */ + ext_vbus = extcon_get_edev_by_phandle(dev, 0); + if (IS_ERR(ext_vbus) && PTR_ERR(ext_vbus) != -ENODEV) + return PTR_ERR(ext_vbus); + + ext_id = extcon_get_edev_by_phandle(dev, 1); + if (IS_ERR(ext_id) && PTR_ERR(ext_id) != -ENODEV) + return PTR_ERR(ext_id); + } + + cable = &platdata->vbus_extcon; + cable->nb.notifier_call = ci_cable_notifier; + cable->edev = ext_vbus; + + if (!IS_ERR(ext_vbus)) { + ret = extcon_get_state(cable->edev, EXTCON_USB); + if (ret) + cable->connected = true; + else + cable->connected = false; + } + + cable = &platdata->id_extcon; + cable->nb.notifier_call = ci_cable_notifier; + cable->edev = ext_id; + + if (!IS_ERR(ext_id)) { + ret = extcon_get_state(cable->edev, EXTCON_USB_HOST); + if (ret) + cable->connected = true; + else + cable->connected = false; + } + return 0; +} + +static int ci_extcon_register(struct ci_hdrc *ci) +{ + struct ci_hdrc_cable *id, *vbus; + int ret; + + id = &ci->platdata->id_extcon; + id->ci = ci; + if (!IS_ERR_OR_NULL(id->edev)) { + ret = devm_extcon_register_notifier(ci->dev, id->edev, + EXTCON_USB_HOST, &id->nb); + if (ret < 0) { + dev_err(ci->dev, "register ID failed\n"); + return ret; + } + } + + vbus = &ci->platdata->vbus_extcon; + vbus->ci = ci; + if (!IS_ERR_OR_NULL(vbus->edev)) { + ret = devm_extcon_register_notifier(ci->dev, vbus->edev, + EXTCON_USB, &vbus->nb); + if (ret < 0) { + dev_err(ci->dev, "register VBUS failed\n"); + return ret; + } + } + + return 0; +} + +static DEFINE_IDA(ci_ida); + +struct platform_device *ci_hdrc_add_device(struct device *dev, + struct resource *res, int nres, + struct ci_hdrc_platform_data *platdata) +{ + struct platform_device *pdev; + int id, ret; + + ret = ci_get_platdata(dev, platdata); + if (ret) + return ERR_PTR(ret); + + id = ida_simple_get(&ci_ida, 0, 0, GFP_KERNEL); + if (id < 0) + return ERR_PTR(id); + + pdev = platform_device_alloc("ci_hdrc", id); + if (!pdev) { + ret = -ENOMEM; + goto put_id; + } + + pdev->dev.parent = dev; + + ret = platform_device_add_resources(pdev, res, nres); + if (ret) + goto err; + + ret = platform_device_add_data(pdev, platdata, sizeof(*platdata)); + if (ret) + goto err; + + ret = platform_device_add(pdev); + if (ret) + goto err; + + return pdev; + +err: + platform_device_put(pdev); +put_id: + ida_simple_remove(&ci_ida, id); + return ERR_PTR(ret); +} +EXPORT_SYMBOL_GPL(ci_hdrc_add_device); + +void ci_hdrc_remove_device(struct platform_device *pdev) +{ + int id = pdev->id; + platform_device_unregister(pdev); + ida_simple_remove(&ci_ida, id); +} +EXPORT_SYMBOL_GPL(ci_hdrc_remove_device); + +static inline void ci_role_destroy(struct ci_hdrc *ci) +{ + ci_hdrc_gadget_destroy(ci); + ci_hdrc_host_destroy(ci); + if (ci->is_otg && ci->roles[CI_ROLE_GADGET]) + ci_hdrc_otg_destroy(ci); +} + +static void ci_get_otg_capable(struct ci_hdrc *ci) +{ + if (ci->platdata->flags & CI_HDRC_DUAL_ROLE_NOT_OTG) + ci->is_otg = false; + else + ci->is_otg = (hw_read(ci, CAP_DCCPARAMS, + DCCPARAMS_DC | DCCPARAMS_HC) + == (DCCPARAMS_DC | DCCPARAMS_HC)); + if (ci->is_otg) { + dev_dbg(ci->dev, "It is OTG capable controller\n"); + /* Disable and clear all OTG irq */ + hw_write_otgsc(ci, OTGSC_INT_EN_BITS | OTGSC_INT_STATUS_BITS, + OTGSC_INT_STATUS_BITS); + } +} + +static ssize_t role_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + + if (ci->role != CI_ROLE_END) + return sprintf(buf, "%s\n", ci_role(ci)->name); + + return 0; +} + +static ssize_t role_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t n) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + enum ci_role role; + int ret; + + if (!(ci->roles[CI_ROLE_HOST] && ci->roles[CI_ROLE_GADGET])) { + dev_warn(dev, "Current configuration is not dual-role, quit\n"); + return -EPERM; + } + + for (role = CI_ROLE_HOST; role < CI_ROLE_END; role++) + if (!strncmp(buf, ci->roles[role]->name, + strlen(ci->roles[role]->name))) + break; + + if (role == CI_ROLE_END || role == ci->role) + return -EINVAL; + + pm_runtime_get_sync(dev); + disable_irq(ci->irq); + ci_role_stop(ci); + ret = ci_role_start(ci, role); + if (!ret && ci->role == CI_ROLE_GADGET) + ci_handle_vbus_change(ci); + enable_irq(ci->irq); + pm_runtime_put_sync(dev); + + return (ret == 0) ? n : ret; +} +static DEVICE_ATTR_RW(role); + +static struct attribute *ci_attrs[] = { + &dev_attr_role.attr, + NULL, +}; + +static const struct attribute_group ci_attr_group = { + .attrs = ci_attrs, +}; + +static int ci_hdrc_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct ci_hdrc *ci; + struct resource *res; + void __iomem *base; + int ret; + enum usb_dr_mode dr_mode; + + if (!dev_get_platdata(dev)) { + dev_err(dev, "platform data missing\n"); + return -ENODEV; + } + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + base = devm_ioremap_resource(dev, res); + if (IS_ERR(base)) + return PTR_ERR(base); + + ci = devm_kzalloc(dev, sizeof(*ci), GFP_KERNEL); + if (!ci) + return -ENOMEM; + + spin_lock_init(&ci->lock); + ci->dev = dev; + ci->platdata = dev_get_platdata(dev); + ci->imx28_write_fix = !!(ci->platdata->flags & + CI_HDRC_IMX28_WRITE_FIX); + ci->supports_runtime_pm = !!(ci->platdata->flags & + CI_HDRC_SUPPORTS_RUNTIME_PM); + platform_set_drvdata(pdev, ci); + + ret = hw_device_init(ci, base); + if (ret < 0) { + dev_err(dev, "can't initialize hardware\n"); + return -ENODEV; + } + + ret = ci_ulpi_init(ci); + if (ret) + return ret; + + if (ci->platdata->phy) { + ci->phy = ci->platdata->phy; + } else if (ci->platdata->usb_phy) { + ci->usb_phy = ci->platdata->usb_phy; + } else { + ci->usb_phy = devm_usb_get_phy_by_phandle(dev->parent, "phys", + 0); + ci->phy = devm_phy_get(dev->parent, "usb-phy"); + + /* Fallback to grabbing any registered USB2 PHY */ + if (IS_ERR(ci->usb_phy) && + PTR_ERR(ci->usb_phy) != -EPROBE_DEFER) + ci->usb_phy = devm_usb_get_phy(dev->parent, + USB_PHY_TYPE_USB2); + + /* if both generic PHY and USB PHY layers aren't enabled */ + if (PTR_ERR(ci->phy) == -ENOSYS && + PTR_ERR(ci->usb_phy) == -ENXIO) { + ret = -ENXIO; + goto ulpi_exit; + } + + if (IS_ERR(ci->phy) && IS_ERR(ci->usb_phy)) { + ret = -EPROBE_DEFER; + goto ulpi_exit; + } + + if (IS_ERR(ci->phy)) + ci->phy = NULL; + else if (IS_ERR(ci->usb_phy)) + ci->usb_phy = NULL; + } + + ret = ci_usb_phy_init(ci); + if (ret) { + dev_err(dev, "unable to init phy: %d\n", ret); + return ret; + } + + ci->hw_bank.phys = res->start; + + ci->irq = platform_get_irq(pdev, 0); + if (ci->irq < 0) { + dev_err(dev, "missing IRQ\n"); + ret = ci->irq; + goto deinit_phy; + } + + ci_get_otg_capable(ci); + + dr_mode = ci->platdata->dr_mode; + /* initialize role(s) before the interrupt is requested */ + if (dr_mode == USB_DR_MODE_OTG || dr_mode == USB_DR_MODE_HOST) { + ret = ci_hdrc_host_init(ci); + if (ret) { + if (ret == -ENXIO) + dev_info(dev, "doesn't support host\n"); + else + goto deinit_phy; + } + } + + if (dr_mode == USB_DR_MODE_OTG || dr_mode == USB_DR_MODE_PERIPHERAL) { + ret = ci_hdrc_gadget_init(ci); + if (ret) { + if (ret == -ENXIO) + dev_info(dev, "doesn't support gadget\n"); + else + goto deinit_host; + } + } + + if (!ci->roles[CI_ROLE_HOST] && !ci->roles[CI_ROLE_GADGET]) { + dev_err(dev, "no supported roles\n"); + ret = -ENODEV; + goto deinit_gadget; + } + + if (ci->is_otg && ci->roles[CI_ROLE_GADGET]) { + ret = ci_hdrc_otg_init(ci); + if (ret) { + dev_err(dev, "init otg fails, ret = %d\n", ret); + goto deinit_gadget; + } + } + + if (ci->roles[CI_ROLE_HOST] && ci->roles[CI_ROLE_GADGET]) { + if (ci->is_otg) { + ci->role = ci_otg_role(ci); + /* Enable ID change irq */ + hw_write_otgsc(ci, OTGSC_IDIE, OTGSC_IDIE); + } else { + /* + * If the controller is not OTG capable, but support + * role switch, the defalt role is gadget, and the + * user can switch it through debugfs. + */ + ci->role = CI_ROLE_GADGET; + } + } else { + ci->role = ci->roles[CI_ROLE_HOST] + ? CI_ROLE_HOST + : CI_ROLE_GADGET; + } + + if (!ci_otg_is_fsm_mode(ci)) { + /* only update vbus status for peripheral */ + if (ci->role == CI_ROLE_GADGET) + ci_handle_vbus_change(ci); + + ret = ci_role_start(ci, ci->role); + if (ret) { + dev_err(dev, "can't start %s role\n", + ci_role(ci)->name); + goto stop; + } + } + + ret = devm_request_irq(dev, ci->irq, ci_irq_handler, IRQF_SHARED, + ci->platdata->name, ci); + if (ret) + goto stop; + + ret = ci_extcon_register(ci); + if (ret) + goto stop; + + if (ci->supports_runtime_pm) { + pm_runtime_set_active(&pdev->dev); + pm_runtime_enable(&pdev->dev); + pm_runtime_set_autosuspend_delay(&pdev->dev, 2000); + pm_runtime_mark_last_busy(ci->dev); + pm_runtime_use_autosuspend(&pdev->dev); + } + + if (ci_otg_is_fsm_mode(ci)) + ci_hdrc_otg_fsm_start(ci); + + device_set_wakeup_capable(&pdev->dev, true); + dbg_create_files(ci); + + ret = sysfs_create_group(&dev->kobj, &ci_attr_group); + if (ret) + goto remove_debug; + + return 0; + +remove_debug: + dbg_remove_files(ci); +stop: + if (ci->is_otg && ci->roles[CI_ROLE_GADGET]) + ci_hdrc_otg_destroy(ci); +deinit_gadget: + ci_hdrc_gadget_destroy(ci); +deinit_host: + ci_hdrc_host_destroy(ci); +deinit_phy: + ci_usb_phy_exit(ci); +ulpi_exit: + ci_ulpi_exit(ci); + + return ret; +} + +static int ci_hdrc_remove(struct platform_device *pdev) +{ + struct ci_hdrc *ci = platform_get_drvdata(pdev); + + if (ci->supports_runtime_pm) { + pm_runtime_get_sync(&pdev->dev); + pm_runtime_disable(&pdev->dev); + pm_runtime_put_noidle(&pdev->dev); + } + + dbg_remove_files(ci); + sysfs_remove_group(&ci->dev->kobj, &ci_attr_group); + ci_role_destroy(ci); + ci_hdrc_enter_lpm(ci, true); + ci_usb_phy_exit(ci); + ci_ulpi_exit(ci); + + return 0; +} + +#ifdef CONFIG_PM +/* Prepare wakeup by SRP before suspend */ +static void ci_otg_fsm_suspend_for_srp(struct ci_hdrc *ci) +{ + if ((ci->fsm.otg->state == OTG_STATE_A_IDLE) && + !hw_read_otgsc(ci, OTGSC_ID)) { + hw_write(ci, OP_PORTSC, PORTSC_W1C_BITS | PORTSC_PP, + PORTSC_PP); + hw_write(ci, OP_PORTSC, PORTSC_W1C_BITS | PORTSC_WKCN, + PORTSC_WKCN); + } +} + +/* Handle SRP when wakeup by data pulse */ +static void ci_otg_fsm_wakeup_by_srp(struct ci_hdrc *ci) +{ + if ((ci->fsm.otg->state == OTG_STATE_A_IDLE) && + (ci->fsm.a_bus_drop == 1) && (ci->fsm.a_bus_req == 0)) { + if (!hw_read_otgsc(ci, OTGSC_ID)) { + ci->fsm.a_srp_det = 1; + ci->fsm.a_bus_drop = 0; + } else { + ci->fsm.id = 1; + } + ci_otg_queue_work(ci); + } +} + +static void ci_controller_suspend(struct ci_hdrc *ci) +{ + disable_irq(ci->irq); + ci_hdrc_enter_lpm(ci, true); + if (ci->platdata->phy_clkgate_delay_us) + usleep_range(ci->platdata->phy_clkgate_delay_us, + ci->platdata->phy_clkgate_delay_us + 50); + usb_phy_set_suspend(ci->usb_phy, 1); + ci->in_lpm = true; + enable_irq(ci->irq); +} + +/* + * Handle the wakeup interrupt triggered by extcon connector + * We need to call ci_irq again for extcon since the first + * interrupt (wakeup int) only let the controller be out of + * low power mode, but not handle any interrupts. + */ +static void ci_extcon_wakeup_int(struct ci_hdrc *ci) +{ + struct ci_hdrc_cable *cable_id, *cable_vbus; + u32 otgsc = hw_read_otgsc(ci, ~0); + + cable_id = &ci->platdata->id_extcon; + cable_vbus = &ci->platdata->vbus_extcon; + + if (!IS_ERR(cable_id->edev) && ci->is_otg && + (otgsc & OTGSC_IDIE) && (otgsc & OTGSC_IDIS)) + ci_irq(ci); + + if (!IS_ERR(cable_vbus->edev) && ci->is_otg && + (otgsc & OTGSC_BSVIE) && (otgsc & OTGSC_BSVIS)) + ci_irq(ci); +} + +static int ci_controller_resume(struct device *dev) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + int ret; + + dev_dbg(dev, "at %s\n", __func__); + + if (!ci->in_lpm) { + WARN_ON(1); + return 0; + } + + ci_hdrc_enter_lpm(ci, false); + + ret = ci_ulpi_resume(ci); + if (ret) + return ret; + + if (ci->usb_phy) { + usb_phy_set_suspend(ci->usb_phy, 0); + usb_phy_set_wakeup(ci->usb_phy, false); + hw_wait_phy_stable(); + } + + ci->in_lpm = false; + if (ci->wakeup_int) { + ci->wakeup_int = false; + pm_runtime_mark_last_busy(ci->dev); + pm_runtime_put_autosuspend(ci->dev); + enable_irq(ci->irq); + if (ci_otg_is_fsm_mode(ci)) + ci_otg_fsm_wakeup_by_srp(ci); + ci_extcon_wakeup_int(ci); + } + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int ci_suspend(struct device *dev) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + + if (ci->wq) + flush_workqueue(ci->wq); + /* + * Controller needs to be active during suspend, otherwise the core + * may run resume when the parent is at suspend if other driver's + * suspend fails, it occurs before parent's suspend has not started, + * but the core suspend has finished. + */ + if (ci->in_lpm) + pm_runtime_resume(dev); + + if (ci->in_lpm) { + WARN_ON(1); + return 0; + } + + if (device_may_wakeup(dev)) { + if (ci_otg_is_fsm_mode(ci)) + ci_otg_fsm_suspend_for_srp(ci); + + usb_phy_set_wakeup(ci->usb_phy, true); + enable_irq_wake(ci->irq); + } + + ci_controller_suspend(ci); + + return 0; +} + +static int ci_resume(struct device *dev) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + int ret; + + if (device_may_wakeup(dev)) + disable_irq_wake(ci->irq); + + ret = ci_controller_resume(dev); + if (ret) + return ret; + + if (ci->supports_runtime_pm) { + pm_runtime_disable(dev); + pm_runtime_set_active(dev); + pm_runtime_enable(dev); + } + + return ret; +} +#endif /* CONFIG_PM_SLEEP */ + +static int ci_runtime_suspend(struct device *dev) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + + dev_dbg(dev, "at %s\n", __func__); + + if (ci->in_lpm) { + WARN_ON(1); + return 0; + } + + if (ci_otg_is_fsm_mode(ci)) + ci_otg_fsm_suspend_for_srp(ci); + + usb_phy_set_wakeup(ci->usb_phy, true); + ci_controller_suspend(ci); + + return 0; +} + +static int ci_runtime_resume(struct device *dev) +{ + return ci_controller_resume(dev); +} + +#endif /* CONFIG_PM */ +static const struct dev_pm_ops ci_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(ci_suspend, ci_resume) + SET_RUNTIME_PM_OPS(ci_runtime_suspend, ci_runtime_resume, NULL) +}; + +static struct platform_driver ci_hdrc_driver = { + .probe = ci_hdrc_probe, + .remove = ci_hdrc_remove, + .driver = { + .name = "ci_hdrc", + .pm = &ci_pm_ops, + }, +}; + +static int __init ci_hdrc_platform_register(void) +{ + ci_hdrc_host_driver_init(); + return platform_driver_register(&ci_hdrc_driver); +} +module_init(ci_hdrc_platform_register); + +static void __exit ci_hdrc_platform_unregister(void) +{ + platform_driver_unregister(&ci_hdrc_driver); +} +module_exit(ci_hdrc_platform_unregister); + +MODULE_ALIAS("platform:ci_hdrc"); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("David Lopo <dlopo@chipidea.mips.com>"); +MODULE_DESCRIPTION("ChipIdea HDRC Driver"); diff --git a/drivers/usb/chipidea/debug.c b/drivers/usb/chipidea/debug.c new file mode 100644 index 000000000..fcc91a338 --- /dev/null +++ b/drivers/usb/chipidea/debug.c @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <linux/kernel.h> +#include <linux/device.h> +#include <linux/types.h> +#include <linux/spinlock.h> +#include <linux/debugfs.h> +#include <linux/seq_file.h> +#include <linux/uaccess.h> +#include <linux/usb/ch9.h> +#include <linux/usb/gadget.h> +#include <linux/usb/phy.h> +#include <linux/usb/otg.h> +#include <linux/usb/otg-fsm.h> +#include <linux/usb/chipidea.h> + +#include "ci.h" +#include "udc.h" +#include "bits.h" +#include "otg.h" + +/** + * ci_device_show: prints information about device capabilities and status + */ +static int ci_device_show(struct seq_file *s, void *data) +{ + struct ci_hdrc *ci = s->private; + struct usb_gadget *gadget = &ci->gadget; + + seq_printf(s, "speed = %d\n", gadget->speed); + seq_printf(s, "max_speed = %d\n", gadget->max_speed); + seq_printf(s, "is_otg = %d\n", gadget->is_otg); + seq_printf(s, "is_a_peripheral = %d\n", gadget->is_a_peripheral); + seq_printf(s, "b_hnp_enable = %d\n", gadget->b_hnp_enable); + seq_printf(s, "a_hnp_support = %d\n", gadget->a_hnp_support); + seq_printf(s, "a_alt_hnp_support = %d\n", gadget->a_alt_hnp_support); + seq_printf(s, "name = %s\n", + (gadget->name ? gadget->name : "")); + + if (!ci->driver) + return 0; + + seq_printf(s, "gadget function = %s\n", + (ci->driver->function ? ci->driver->function : "")); + seq_printf(s, "gadget max speed = %d\n", ci->driver->max_speed); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(ci_device); + +/** + * ci_port_test_show: reads port test mode + */ +static int ci_port_test_show(struct seq_file *s, void *data) +{ + struct ci_hdrc *ci = s->private; + unsigned long flags; + unsigned mode; + + pm_runtime_get_sync(ci->dev); + spin_lock_irqsave(&ci->lock, flags); + mode = hw_port_test_get(ci); + spin_unlock_irqrestore(&ci->lock, flags); + pm_runtime_put_sync(ci->dev); + + seq_printf(s, "mode = %u\n", mode); + + return 0; +} + +/** + * ci_port_test_write: writes port test mode + */ +static ssize_t ci_port_test_write(struct file *file, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct ci_hdrc *ci = s->private; + unsigned long flags; + unsigned mode; + char buf[32]; + int ret; + + count = min_t(size_t, sizeof(buf) - 1, count); + if (copy_from_user(buf, ubuf, count)) + return -EFAULT; + + /* sscanf requires a zero terminated string */ + buf[count] = '\0'; + + if (sscanf(buf, "%u", &mode) != 1) + return -EINVAL; + + if (mode > 255) + return -EBADRQC; + + pm_runtime_get_sync(ci->dev); + spin_lock_irqsave(&ci->lock, flags); + ret = hw_port_test_set(ci, mode); + spin_unlock_irqrestore(&ci->lock, flags); + pm_runtime_put_sync(ci->dev); + + return ret ? ret : count; +} + +static int ci_port_test_open(struct inode *inode, struct file *file) +{ + return single_open(file, ci_port_test_show, inode->i_private); +} + +static const struct file_operations ci_port_test_fops = { + .open = ci_port_test_open, + .write = ci_port_test_write, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +/** + * ci_qheads_show: DMA contents of all queue heads + */ +static int ci_qheads_show(struct seq_file *s, void *data) +{ + struct ci_hdrc *ci = s->private; + unsigned long flags; + unsigned i, j; + + if (ci->role != CI_ROLE_GADGET) { + seq_printf(s, "not in gadget mode\n"); + return 0; + } + + spin_lock_irqsave(&ci->lock, flags); + for (i = 0; i < ci->hw_ep_max/2; i++) { + struct ci_hw_ep *hweprx = &ci->ci_hw_ep[i]; + struct ci_hw_ep *hweptx = + &ci->ci_hw_ep[i + ci->hw_ep_max/2]; + seq_printf(s, "EP=%02i: RX=%08X TX=%08X\n", + i, (u32)hweprx->qh.dma, (u32)hweptx->qh.dma); + for (j = 0; j < (sizeof(struct ci_hw_qh)/sizeof(u32)); j++) + seq_printf(s, " %04X: %08X %08X\n", j, + *((u32 *)hweprx->qh.ptr + j), + *((u32 *)hweptx->qh.ptr + j)); + } + spin_unlock_irqrestore(&ci->lock, flags); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(ci_qheads); + +/** + * ci_requests_show: DMA contents of all requests currently queued (all endpts) + */ +static int ci_requests_show(struct seq_file *s, void *data) +{ + struct ci_hdrc *ci = s->private; + unsigned long flags; + struct ci_hw_req *req = NULL; + struct td_node *node, *tmpnode; + unsigned i, j, qsize = sizeof(struct ci_hw_td)/sizeof(u32); + + if (ci->role != CI_ROLE_GADGET) { + seq_printf(s, "not in gadget mode\n"); + return 0; + } + + spin_lock_irqsave(&ci->lock, flags); + for (i = 0; i < ci->hw_ep_max; i++) + list_for_each_entry(req, &ci->ci_hw_ep[i].qh.queue, queue) { + list_for_each_entry_safe(node, tmpnode, &req->tds, td) { + seq_printf(s, "EP=%02i: TD=%08X %s\n", + i % (ci->hw_ep_max / 2), + (u32)node->dma, + ((i < ci->hw_ep_max/2) ? + "RX" : "TX")); + + for (j = 0; j < qsize; j++) + seq_printf(s, " %04X: %08X\n", j, + *((u32 *)node->ptr + j)); + } + } + spin_unlock_irqrestore(&ci->lock, flags); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(ci_requests); + +static int ci_otg_show(struct seq_file *s, void *unused) +{ + struct ci_hdrc *ci = s->private; + struct otg_fsm *fsm; + + if (!ci || !ci_otg_is_fsm_mode(ci)) + return 0; + + fsm = &ci->fsm; + + /* ------ State ----- */ + seq_printf(s, "OTG state: %s\n\n", + usb_otg_state_string(ci->otg.state)); + + /* ------ State Machine Variables ----- */ + seq_printf(s, "a_bus_drop: %d\n", fsm->a_bus_drop); + + seq_printf(s, "a_bus_req: %d\n", fsm->a_bus_req); + + seq_printf(s, "a_srp_det: %d\n", fsm->a_srp_det); + + seq_printf(s, "a_vbus_vld: %d\n", fsm->a_vbus_vld); + + seq_printf(s, "b_conn: %d\n", fsm->b_conn); + + seq_printf(s, "adp_change: %d\n", fsm->adp_change); + + seq_printf(s, "power_up: %d\n", fsm->power_up); + + seq_printf(s, "a_bus_resume: %d\n", fsm->a_bus_resume); + + seq_printf(s, "a_bus_suspend: %d\n", fsm->a_bus_suspend); + + seq_printf(s, "a_conn: %d\n", fsm->a_conn); + + seq_printf(s, "b_bus_req: %d\n", fsm->b_bus_req); + + seq_printf(s, "b_bus_suspend: %d\n", fsm->b_bus_suspend); + + seq_printf(s, "b_se0_srp: %d\n", fsm->b_se0_srp); + + seq_printf(s, "b_ssend_srp: %d\n", fsm->b_ssend_srp); + + seq_printf(s, "b_sess_vld: %d\n", fsm->b_sess_vld); + + seq_printf(s, "b_srp_done: %d\n", fsm->b_srp_done); + + seq_printf(s, "drv_vbus: %d\n", fsm->drv_vbus); + + seq_printf(s, "loc_conn: %d\n", fsm->loc_conn); + + seq_printf(s, "loc_sof: %d\n", fsm->loc_sof); + + seq_printf(s, "adp_prb: %d\n", fsm->adp_prb); + + seq_printf(s, "id: %d\n", fsm->id); + + seq_printf(s, "protocol: %d\n", fsm->protocol); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(ci_otg); + +static int ci_role_show(struct seq_file *s, void *data) +{ + struct ci_hdrc *ci = s->private; + + if (ci->role != CI_ROLE_END) + seq_printf(s, "%s\n", ci_role(ci)->name); + + return 0; +} + +static ssize_t ci_role_write(struct file *file, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct seq_file *s = file->private_data; + struct ci_hdrc *ci = s->private; + enum ci_role role; + char buf[8]; + int ret; + + if (copy_from_user(buf, ubuf, min_t(size_t, sizeof(buf) - 1, count))) + return -EFAULT; + + for (role = CI_ROLE_HOST; role < CI_ROLE_END; role++) + if (ci->roles[role] && + !strncmp(buf, ci->roles[role]->name, + strlen(ci->roles[role]->name))) + break; + + if (role == CI_ROLE_END || role == ci->role) + return -EINVAL; + + pm_runtime_get_sync(ci->dev); + disable_irq(ci->irq); + ci_role_stop(ci); + ret = ci_role_start(ci, role); + enable_irq(ci->irq); + pm_runtime_put_sync(ci->dev); + + return ret ? ret : count; +} + +static int ci_role_open(struct inode *inode, struct file *file) +{ + return single_open(file, ci_role_show, inode->i_private); +} + +static const struct file_operations ci_role_fops = { + .open = ci_role_open, + .write = ci_role_write, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static int ci_registers_show(struct seq_file *s, void *unused) +{ + struct ci_hdrc *ci = s->private; + u32 tmp_reg; + + if (!ci || ci->in_lpm) + return -EPERM; + + /* ------ Registers ----- */ + tmp_reg = hw_read_intr_enable(ci); + seq_printf(s, "USBINTR reg: %08x\n", tmp_reg); + + tmp_reg = hw_read_intr_status(ci); + seq_printf(s, "USBSTS reg: %08x\n", tmp_reg); + + tmp_reg = hw_read(ci, OP_USBMODE, ~0); + seq_printf(s, "USBMODE reg: %08x\n", tmp_reg); + + tmp_reg = hw_read(ci, OP_USBCMD, ~0); + seq_printf(s, "USBCMD reg: %08x\n", tmp_reg); + + tmp_reg = hw_read(ci, OP_PORTSC, ~0); + seq_printf(s, "PORTSC reg: %08x\n", tmp_reg); + + if (ci->is_otg) { + tmp_reg = hw_read_otgsc(ci, ~0); + seq_printf(s, "OTGSC reg: %08x\n", tmp_reg); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(ci_registers); + +/** + * dbg_create_files: initializes the attribute interface + * @ci: device + * + * This function returns an error code + */ +void dbg_create_files(struct ci_hdrc *ci) +{ + ci->debugfs = debugfs_create_dir(dev_name(ci->dev), NULL); + + debugfs_create_file("device", S_IRUGO, ci->debugfs, ci, + &ci_device_fops); + debugfs_create_file("port_test", S_IRUGO | S_IWUSR, ci->debugfs, ci, + &ci_port_test_fops); + debugfs_create_file("qheads", S_IRUGO, ci->debugfs, ci, + &ci_qheads_fops); + debugfs_create_file("requests", S_IRUGO, ci->debugfs, ci, + &ci_requests_fops); + + if (ci_otg_is_fsm_mode(ci)) { + debugfs_create_file("otg", S_IRUGO, ci->debugfs, ci, + &ci_otg_fops); + } + + debugfs_create_file("role", S_IRUGO | S_IWUSR, ci->debugfs, ci, + &ci_role_fops); + debugfs_create_file("registers", S_IRUGO, ci->debugfs, ci, + &ci_registers_fops); +} + +/** + * dbg_remove_files: destroys the attribute interface + * @ci: device + */ +void dbg_remove_files(struct ci_hdrc *ci) +{ + debugfs_remove_recursive(ci->debugfs); +} diff --git a/drivers/usb/chipidea/host.c b/drivers/usb/chipidea/host.c new file mode 100644 index 000000000..71b26d0d1 --- /dev/null +++ b/drivers/usb/chipidea/host.c @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * host.c - ChipIdea USB host controller driver + * + * Copyright (c) 2012 Intel Corporation + * + * Author: Alexander Shishkin + */ + +#include <linux/kernel.h> +#include <linux/io.h> +#include <linux/usb.h> +#include <linux/usb/hcd.h> +#include <linux/usb/chipidea.h> +#include <linux/regulator/consumer.h> + +#include "../host/ehci.h" + +#include "ci.h" +#include "bits.h" +#include "host.h" + +static struct hc_driver __read_mostly ci_ehci_hc_driver; +static int (*orig_bus_suspend)(struct usb_hcd *hcd); + +struct ehci_ci_priv { + struct regulator *reg_vbus; + bool enabled; +}; + +static int ehci_ci_portpower(struct usb_hcd *hcd, int portnum, bool enable) +{ + struct ehci_hcd *ehci = hcd_to_ehci(hcd); + struct ehci_ci_priv *priv = (struct ehci_ci_priv *)ehci->priv; + struct device *dev = hcd->self.controller; + struct ci_hdrc *ci = dev_get_drvdata(dev); + int ret = 0; + int port = HCS_N_PORTS(ehci->hcs_params); + + if (priv->reg_vbus && enable != priv->enabled) { + if (port > 1) { + dev_warn(dev, + "Not support multi-port regulator control\n"); + return 0; + } + if (enable) + ret = regulator_enable(priv->reg_vbus); + else + ret = regulator_disable(priv->reg_vbus); + if (ret) { + dev_err(dev, + "Failed to %s vbus regulator, ret=%d\n", + enable ? "enable" : "disable", ret); + return ret; + } + priv->enabled = enable; + } + + if (enable && (ci->platdata->phy_mode == USBPHY_INTERFACE_MODE_HSIC)) { + /* + * Marvell 28nm HSIC PHY requires forcing the port to HS mode. + * As HSIC is always HS, this should be safe for others. + */ + hw_port_test_set(ci, 5); + hw_port_test_set(ci, 0); + } + return 0; +}; + +static int ehci_ci_reset(struct usb_hcd *hcd) +{ + struct device *dev = hcd->self.controller; + struct ci_hdrc *ci = dev_get_drvdata(dev); + struct ehci_hcd *ehci = hcd_to_ehci(hcd); + int ret; + + ret = ehci_setup(hcd); + if (ret) + return ret; + + ehci->need_io_watchdog = 0; + + if (ci->platdata->notify_event) { + ret = ci->platdata->notify_event(ci, + CI_HDRC_CONTROLLER_RESET_EVENT); + if (ret) + return ret; + } + + ci_platform_configure(ci); + + return ret; +} + +static const struct ehci_driver_overrides ehci_ci_overrides = { + .extra_priv_size = sizeof(struct ehci_ci_priv), + .port_power = ehci_ci_portpower, + .reset = ehci_ci_reset, +}; + +static irqreturn_t host_irq(struct ci_hdrc *ci) +{ + return usb_hcd_irq(ci->irq, ci->hcd); +} + +static int host_start(struct ci_hdrc *ci) +{ + struct usb_hcd *hcd; + struct ehci_hcd *ehci; + struct ehci_ci_priv *priv; + int ret; + + if (usb_disabled()) + return -ENODEV; + + hcd = __usb_create_hcd(&ci_ehci_hc_driver, ci->dev->parent, + ci->dev, dev_name(ci->dev), NULL); + if (!hcd) + return -ENOMEM; + + dev_set_drvdata(ci->dev, ci); + hcd->rsrc_start = ci->hw_bank.phys; + hcd->rsrc_len = ci->hw_bank.size; + hcd->regs = ci->hw_bank.abs; + hcd->has_tt = 1; + + hcd->power_budget = ci->platdata->power_budget; + hcd->tpl_support = ci->platdata->tpl_support; + if (ci->phy || ci->usb_phy) { + hcd->skip_phy_initialization = 1; + if (ci->usb_phy) + hcd->usb_phy = ci->usb_phy; + } + + ehci = hcd_to_ehci(hcd); + ehci->caps = ci->hw_bank.cap; + ehci->has_hostpc = ci->hw_bank.lpm; + ehci->has_tdi_phy_lpm = ci->hw_bank.lpm; + ehci->imx28_write_fix = ci->imx28_write_fix; + + priv = (struct ehci_ci_priv *)ehci->priv; + priv->reg_vbus = NULL; + + if (ci->platdata->reg_vbus && !ci_otg_is_fsm_mode(ci)) { + if (ci->platdata->flags & CI_HDRC_TURN_VBUS_EARLY_ON) { + ret = regulator_enable(ci->platdata->reg_vbus); + if (ret) { + dev_err(ci->dev, + "Failed to enable vbus regulator, ret=%d\n", + ret); + goto put_hcd; + } + } else { + priv->reg_vbus = ci->platdata->reg_vbus; + } + } + + ret = usb_add_hcd(hcd, 0, 0); + if (ret) { + goto disable_reg; + } else { + struct usb_otg *otg = &ci->otg; + + ci->hcd = hcd; + + if (ci_otg_is_fsm_mode(ci)) { + otg->host = &hcd->self; + hcd->self.otg_port = 1; + } + } + + return ret; + +disable_reg: + if (ci->platdata->reg_vbus && !ci_otg_is_fsm_mode(ci) && + (ci->platdata->flags & CI_HDRC_TURN_VBUS_EARLY_ON)) + regulator_disable(ci->platdata->reg_vbus); +put_hcd: + usb_put_hcd(hcd); + + return ret; +} + +static void host_stop(struct ci_hdrc *ci) +{ + struct usb_hcd *hcd = ci->hcd; + + if (hcd) { + if (ci->platdata->notify_event) + ci->platdata->notify_event(ci, + CI_HDRC_CONTROLLER_STOPPED_EVENT); + usb_remove_hcd(hcd); + ci->role = CI_ROLE_END; + synchronize_irq(ci->irq); + usb_put_hcd(hcd); + if (ci->platdata->reg_vbus && !ci_otg_is_fsm_mode(ci) && + (ci->platdata->flags & CI_HDRC_TURN_VBUS_EARLY_ON)) + regulator_disable(ci->platdata->reg_vbus); + } + ci->hcd = NULL; + ci->otg.host = NULL; +} + + +void ci_hdrc_host_destroy(struct ci_hdrc *ci) +{ + if (ci->role == CI_ROLE_HOST && ci->hcd) + host_stop(ci); +} + +static int ci_ehci_bus_suspend(struct usb_hcd *hcd) +{ + struct ehci_hcd *ehci = hcd_to_ehci(hcd); + int port; + u32 tmp; + + int ret = orig_bus_suspend(hcd); + + if (ret) + return ret; + + port = HCS_N_PORTS(ehci->hcs_params); + while (port--) { + u32 __iomem *reg = &ehci->regs->port_status[port]; + u32 portsc = ehci_readl(ehci, reg); + + if (portsc & PORT_CONNECT) { + /* + * For chipidea, the resume signal will be ended + * automatically, so for remote wakeup case, the + * usbcmd.rs may not be set before the resume has + * ended if other resume paths consumes too much + * time (~24ms), in that case, the SOF will not + * send out within 3ms after resume ends, then the + * high speed device will enter full speed mode. + */ + + tmp = ehci_readl(ehci, &ehci->regs->command); + tmp |= CMD_RUN; + ehci_writel(ehci, tmp, &ehci->regs->command); + /* + * It needs a short delay between set RS bit and PHCD. + */ + usleep_range(150, 200); + break; + } + } + + return 0; +} + +int ci_hdrc_host_init(struct ci_hdrc *ci) +{ + struct ci_role_driver *rdrv; + + if (!hw_read(ci, CAP_DCCPARAMS, DCCPARAMS_HC)) + return -ENXIO; + + rdrv = devm_kzalloc(ci->dev, sizeof(struct ci_role_driver), GFP_KERNEL); + if (!rdrv) + return -ENOMEM; + + rdrv->start = host_start; + rdrv->stop = host_stop; + rdrv->irq = host_irq; + rdrv->name = "host"; + ci->roles[CI_ROLE_HOST] = rdrv; + + return 0; +} + +void ci_hdrc_host_driver_init(void) +{ + ehci_init_driver(&ci_ehci_hc_driver, &ehci_ci_overrides); + orig_bus_suspend = ci_ehci_hc_driver.bus_suspend; + ci_ehci_hc_driver.bus_suspend = ci_ehci_bus_suspend; +} diff --git a/drivers/usb/chipidea/host.h b/drivers/usb/chipidea/host.h new file mode 100644 index 000000000..70112cf0f --- /dev/null +++ b/drivers/usb/chipidea/host.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __DRIVERS_USB_CHIPIDEA_HOST_H +#define __DRIVERS_USB_CHIPIDEA_HOST_H + +#ifdef CONFIG_USB_CHIPIDEA_HOST + +int ci_hdrc_host_init(struct ci_hdrc *ci); +void ci_hdrc_host_destroy(struct ci_hdrc *ci); +void ci_hdrc_host_driver_init(void); + +#else + +static inline int ci_hdrc_host_init(struct ci_hdrc *ci) +{ + return -ENXIO; +} + +static inline void ci_hdrc_host_destroy(struct ci_hdrc *ci) +{ + +} + +static void ci_hdrc_host_driver_init(void) +{ + +} + +#endif + +#endif /* __DRIVERS_USB_CHIPIDEA_HOST_H */ diff --git a/drivers/usb/chipidea/otg.c b/drivers/usb/chipidea/otg.c new file mode 100644 index 000000000..f25d4827f --- /dev/null +++ b/drivers/usb/chipidea/otg.c @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * otg.c - ChipIdea USB IP core OTG driver + * + * Copyright (C) 2013 Freescale Semiconductor, Inc. + * + * Author: Peter Chen + */ + +/* + * This file mainly handles otgsc register, OTG fsm operations for HNP and SRP + * are also included. + */ + +#include <linux/usb/otg.h> +#include <linux/usb/gadget.h> +#include <linux/usb/chipidea.h> + +#include "ci.h" +#include "bits.h" +#include "otg.h" +#include "otg_fsm.h" + +/** + * hw_read_otgsc returns otgsc register bits value. + * @mask: bitfield mask + */ +u32 hw_read_otgsc(struct ci_hdrc *ci, u32 mask) +{ + struct ci_hdrc_cable *cable; + u32 val = hw_read(ci, OP_OTGSC, mask); + + /* + * If using extcon framework for VBUS and/or ID signal + * detection overwrite OTGSC register value + */ + cable = &ci->platdata->vbus_extcon; + if (!IS_ERR(cable->edev)) { + if (cable->changed) + val |= OTGSC_BSVIS; + else + val &= ~OTGSC_BSVIS; + + if (cable->connected) + val |= OTGSC_BSV; + else + val &= ~OTGSC_BSV; + + if (cable->enabled) + val |= OTGSC_BSVIE; + else + val &= ~OTGSC_BSVIE; + } + + cable = &ci->platdata->id_extcon; + if (!IS_ERR(cable->edev)) { + if (cable->changed) + val |= OTGSC_IDIS; + else + val &= ~OTGSC_IDIS; + + if (cable->connected) + val &= ~OTGSC_ID; /* host */ + else + val |= OTGSC_ID; /* device */ + + if (cable->enabled) + val |= OTGSC_IDIE; + else + val &= ~OTGSC_IDIE; + } + + return val & mask; +} + +/** + * hw_write_otgsc updates target bits of OTGSC register. + * @mask: bitfield mask + * @data: to be written + */ +void hw_write_otgsc(struct ci_hdrc *ci, u32 mask, u32 data) +{ + struct ci_hdrc_cable *cable; + + cable = &ci->platdata->vbus_extcon; + if (!IS_ERR(cable->edev)) { + if (data & mask & OTGSC_BSVIS) + cable->changed = false; + + /* Don't enable vbus interrupt if using external notifier */ + if (data & mask & OTGSC_BSVIE) { + cable->enabled = true; + data &= ~OTGSC_BSVIE; + } else if (mask & OTGSC_BSVIE) { + cable->enabled = false; + } + } + + cable = &ci->platdata->id_extcon; + if (!IS_ERR(cable->edev)) { + if (data & mask & OTGSC_IDIS) + cable->changed = false; + + /* Don't enable id interrupt if using external notifier */ + if (data & mask & OTGSC_IDIE) { + cable->enabled = true; + data &= ~OTGSC_IDIE; + } else if (mask & OTGSC_IDIE) { + cable->enabled = false; + } + } + + hw_write(ci, OP_OTGSC, mask | OTGSC_INT_STATUS_BITS, data); +} + +/** + * ci_otg_role - pick role based on ID pin state + * @ci: the controller + */ +enum ci_role ci_otg_role(struct ci_hdrc *ci) +{ + enum ci_role role = hw_read_otgsc(ci, OTGSC_ID) + ? CI_ROLE_GADGET + : CI_ROLE_HOST; + + return role; +} + +void ci_handle_vbus_change(struct ci_hdrc *ci) +{ + if (!ci->is_otg) + return; + + if (hw_read_otgsc(ci, OTGSC_BSV) && !ci->vbus_active) + usb_gadget_vbus_connect(&ci->gadget); + else if (!hw_read_otgsc(ci, OTGSC_BSV) && ci->vbus_active) + usb_gadget_vbus_disconnect(&ci->gadget); +} + +/** + * When we switch to device mode, the vbus value should be lower + * than OTGSC_BSV before connecting to host. + * + * @ci: the controller + * + * This function returns an error code if timeout + */ +static int hw_wait_vbus_lower_bsv(struct ci_hdrc *ci) +{ + unsigned long elapse = jiffies + msecs_to_jiffies(5000); + u32 mask = OTGSC_BSV; + + while (hw_read_otgsc(ci, mask)) { + if (time_after(jiffies, elapse)) { + dev_err(ci->dev, "timeout waiting for %08x in OTGSC\n", + mask); + return -ETIMEDOUT; + } + msleep(20); + } + + return 0; +} + +static void ci_handle_id_switch(struct ci_hdrc *ci) +{ + enum ci_role role = ci_otg_role(ci); + + if (role != ci->role) { + dev_dbg(ci->dev, "switching from %s to %s\n", + ci_role(ci)->name, ci->roles[role]->name); + + ci_role_stop(ci); + + if (role == CI_ROLE_GADGET && + IS_ERR(ci->platdata->vbus_extcon.edev)) + /* + * Wait vbus lower than OTGSC_BSV before connecting + * to host. If connecting status is from an external + * connector instead of register, we don't need to + * care vbus on the board, since it will not affect + * external connector status. + */ + hw_wait_vbus_lower_bsv(ci); + + ci_role_start(ci, role); + /* vbus change may have already occurred */ + if (role == CI_ROLE_GADGET) + ci_handle_vbus_change(ci); + } +} +/** + * ci_otg_work - perform otg (vbus/id) event handle + * @work: work struct + */ +static void ci_otg_work(struct work_struct *work) +{ + struct ci_hdrc *ci = container_of(work, struct ci_hdrc, work); + + if (ci_otg_is_fsm_mode(ci) && !ci_otg_fsm_work(ci)) { + enable_irq(ci->irq); + return; + } + + pm_runtime_get_sync(ci->dev); + + if (ci->id_event) { + ci->id_event = false; + ci_handle_id_switch(ci); + } + + if (ci->b_sess_valid_event) { + ci->b_sess_valid_event = false; + ci_handle_vbus_change(ci); + } + + pm_runtime_put_sync(ci->dev); + + enable_irq(ci->irq); +} + + +/** + * ci_hdrc_otg_init - initialize otg struct + * ci: the controller + */ +int ci_hdrc_otg_init(struct ci_hdrc *ci) +{ + INIT_WORK(&ci->work, ci_otg_work); + ci->wq = create_freezable_workqueue("ci_otg"); + if (!ci->wq) { + dev_err(ci->dev, "can't create workqueue\n"); + return -ENODEV; + } + + if (ci_otg_is_fsm_mode(ci)) + return ci_hdrc_otg_fsm_init(ci); + + return 0; +} + +/** + * ci_hdrc_otg_destroy - destroy otg struct + * ci: the controller + */ +void ci_hdrc_otg_destroy(struct ci_hdrc *ci) +{ + if (ci->wq) { + flush_workqueue(ci->wq); + destroy_workqueue(ci->wq); + } + /* Disable all OTG irq and clear status */ + hw_write_otgsc(ci, OTGSC_INT_EN_BITS | OTGSC_INT_STATUS_BITS, + OTGSC_INT_STATUS_BITS); + if (ci_otg_is_fsm_mode(ci)) + ci_hdrc_otg_fsm_remove(ci); +} diff --git a/drivers/usb/chipidea/otg.h b/drivers/usb/chipidea/otg.h new file mode 100644 index 000000000..4f8b8179e --- /dev/null +++ b/drivers/usb/chipidea/otg.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2013-2014 Freescale Semiconductor, Inc. + * + * Author: Peter Chen + */ + +#ifndef __DRIVERS_USB_CHIPIDEA_OTG_H +#define __DRIVERS_USB_CHIPIDEA_OTG_H + +u32 hw_read_otgsc(struct ci_hdrc *ci, u32 mask); +void hw_write_otgsc(struct ci_hdrc *ci, u32 mask, u32 data); +int ci_hdrc_otg_init(struct ci_hdrc *ci); +void ci_hdrc_otg_destroy(struct ci_hdrc *ci); +enum ci_role ci_otg_role(struct ci_hdrc *ci); +void ci_handle_vbus_change(struct ci_hdrc *ci); +static inline void ci_otg_queue_work(struct ci_hdrc *ci) +{ + disable_irq_nosync(ci->irq); + if (queue_work(ci->wq, &ci->work) == false) + enable_irq(ci->irq); +} + +#endif /* __DRIVERS_USB_CHIPIDEA_OTG_H */ diff --git a/drivers/usb/chipidea/otg_fsm.c b/drivers/usb/chipidea/otg_fsm.c new file mode 100644 index 000000000..6ed4b00db --- /dev/null +++ b/drivers/usb/chipidea/otg_fsm.c @@ -0,0 +1,844 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * otg_fsm.c - ChipIdea USB IP core OTG FSM driver + * + * Copyright (C) 2014 Freescale Semiconductor, Inc. + * + * Author: Jun Li + */ + +/* + * This file mainly handles OTG fsm, it includes OTG fsm operations + * for HNP and SRP. + * + * TODO List + * - ADP + * - OTG test device + */ + +#include <linux/usb/otg.h> +#include <linux/usb/gadget.h> +#include <linux/usb/hcd.h> +#include <linux/usb/chipidea.h> +#include <linux/regulator/consumer.h> + +#include "ci.h" +#include "bits.h" +#include "otg.h" +#include "otg_fsm.h" + +/* Add for otg: interact with user space app */ +static ssize_t +a_bus_req_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + char *next; + unsigned size, t; + struct ci_hdrc *ci = dev_get_drvdata(dev); + + next = buf; + size = PAGE_SIZE; + t = scnprintf(next, size, "%d\n", ci->fsm.a_bus_req); + size -= t; + next += t; + + return PAGE_SIZE - size; +} + +static ssize_t +a_bus_req_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + + if (count > 2) + return -1; + + mutex_lock(&ci->fsm.lock); + if (buf[0] == '0') { + ci->fsm.a_bus_req = 0; + } else if (buf[0] == '1') { + /* If a_bus_drop is TRUE, a_bus_req can't be set */ + if (ci->fsm.a_bus_drop) { + mutex_unlock(&ci->fsm.lock); + return count; + } + ci->fsm.a_bus_req = 1; + if (ci->fsm.otg->state == OTG_STATE_A_PERIPHERAL) { + ci->gadget.host_request_flag = 1; + mutex_unlock(&ci->fsm.lock); + return count; + } + } + + ci_otg_queue_work(ci); + mutex_unlock(&ci->fsm.lock); + + return count; +} +static DEVICE_ATTR_RW(a_bus_req); + +static ssize_t +a_bus_drop_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + char *next; + unsigned size, t; + struct ci_hdrc *ci = dev_get_drvdata(dev); + + next = buf; + size = PAGE_SIZE; + t = scnprintf(next, size, "%d\n", ci->fsm.a_bus_drop); + size -= t; + next += t; + + return PAGE_SIZE - size; +} + +static ssize_t +a_bus_drop_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + + if (count > 2) + return -1; + + mutex_lock(&ci->fsm.lock); + if (buf[0] == '0') { + ci->fsm.a_bus_drop = 0; + } else if (buf[0] == '1') { + ci->fsm.a_bus_drop = 1; + ci->fsm.a_bus_req = 0; + } + + ci_otg_queue_work(ci); + mutex_unlock(&ci->fsm.lock); + + return count; +} +static DEVICE_ATTR_RW(a_bus_drop); + +static ssize_t +b_bus_req_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + char *next; + unsigned size, t; + struct ci_hdrc *ci = dev_get_drvdata(dev); + + next = buf; + size = PAGE_SIZE; + t = scnprintf(next, size, "%d\n", ci->fsm.b_bus_req); + size -= t; + next += t; + + return PAGE_SIZE - size; +} + +static ssize_t +b_bus_req_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + + if (count > 2) + return -1; + + mutex_lock(&ci->fsm.lock); + if (buf[0] == '0') + ci->fsm.b_bus_req = 0; + else if (buf[0] == '1') { + ci->fsm.b_bus_req = 1; + if (ci->fsm.otg->state == OTG_STATE_B_PERIPHERAL) { + ci->gadget.host_request_flag = 1; + mutex_unlock(&ci->fsm.lock); + return count; + } + } + + ci_otg_queue_work(ci); + mutex_unlock(&ci->fsm.lock); + + return count; +} +static DEVICE_ATTR_RW(b_bus_req); + +static ssize_t +a_clr_err_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + + if (count > 2) + return -1; + + mutex_lock(&ci->fsm.lock); + if (buf[0] == '1') + ci->fsm.a_clr_err = 1; + + ci_otg_queue_work(ci); + mutex_unlock(&ci->fsm.lock); + + return count; +} +static DEVICE_ATTR_WO(a_clr_err); + +static struct attribute *inputs_attrs[] = { + &dev_attr_a_bus_req.attr, + &dev_attr_a_bus_drop.attr, + &dev_attr_b_bus_req.attr, + &dev_attr_a_clr_err.attr, + NULL, +}; + +static const struct attribute_group inputs_attr_group = { + .name = "inputs", + .attrs = inputs_attrs, +}; + +/* + * Keep this list in the same order as timers indexed + * by enum otg_fsm_timer in include/linux/usb/otg-fsm.h + */ +static unsigned otg_timer_ms[] = { + TA_WAIT_VRISE, + TA_WAIT_VFALL, + TA_WAIT_BCON, + TA_AIDL_BDIS, + TB_ASE0_BRST, + TA_BIDL_ADIS, + TB_AIDL_BDIS, + TB_SE0_SRP, + TB_SRP_FAIL, + 0, + TB_DATA_PLS, + TB_SSEND_SRP, +}; + +/* + * Add timer to active timer list + */ +static void ci_otg_add_timer(struct ci_hdrc *ci, enum otg_fsm_timer t) +{ + unsigned long flags, timer_sec, timer_nsec; + + if (t >= NUM_OTG_FSM_TIMERS) + return; + + spin_lock_irqsave(&ci->lock, flags); + timer_sec = otg_timer_ms[t] / MSEC_PER_SEC; + timer_nsec = (otg_timer_ms[t] % MSEC_PER_SEC) * NSEC_PER_MSEC; + ci->hr_timeouts[t] = ktime_add(ktime_get(), + ktime_set(timer_sec, timer_nsec)); + ci->enabled_otg_timer_bits |= (1 << t); + if ((ci->next_otg_timer == NUM_OTG_FSM_TIMERS) || + ktime_after(ci->hr_timeouts[ci->next_otg_timer], + ci->hr_timeouts[t])) { + ci->next_otg_timer = t; + hrtimer_start_range_ns(&ci->otg_fsm_hrtimer, + ci->hr_timeouts[t], NSEC_PER_MSEC, + HRTIMER_MODE_ABS); + } + spin_unlock_irqrestore(&ci->lock, flags); +} + +/* + * Remove timer from active timer list + */ +static void ci_otg_del_timer(struct ci_hdrc *ci, enum otg_fsm_timer t) +{ + unsigned long flags, enabled_timer_bits; + enum otg_fsm_timer cur_timer, next_timer = NUM_OTG_FSM_TIMERS; + + if ((t >= NUM_OTG_FSM_TIMERS) || + !(ci->enabled_otg_timer_bits & (1 << t))) + return; + + spin_lock_irqsave(&ci->lock, flags); + ci->enabled_otg_timer_bits &= ~(1 << t); + if (ci->next_otg_timer == t) { + if (ci->enabled_otg_timer_bits == 0) { + /* No enabled timers after delete it */ + hrtimer_cancel(&ci->otg_fsm_hrtimer); + ci->next_otg_timer = NUM_OTG_FSM_TIMERS; + } else { + /* Find the next timer */ + enabled_timer_bits = ci->enabled_otg_timer_bits; + for_each_set_bit(cur_timer, &enabled_timer_bits, + NUM_OTG_FSM_TIMERS) { + if ((next_timer == NUM_OTG_FSM_TIMERS) || + ktime_before(ci->hr_timeouts[next_timer], + ci->hr_timeouts[cur_timer])) + next_timer = cur_timer; + } + } + } + if (next_timer != NUM_OTG_FSM_TIMERS) { + ci->next_otg_timer = next_timer; + hrtimer_start_range_ns(&ci->otg_fsm_hrtimer, + ci->hr_timeouts[next_timer], NSEC_PER_MSEC, + HRTIMER_MODE_ABS); + } + spin_unlock_irqrestore(&ci->lock, flags); +} + +/* OTG FSM timer handlers */ +static int a_wait_vrise_tmout(struct ci_hdrc *ci) +{ + ci->fsm.a_wait_vrise_tmout = 1; + return 0; +} + +static int a_wait_vfall_tmout(struct ci_hdrc *ci) +{ + ci->fsm.a_wait_vfall_tmout = 1; + return 0; +} + +static int a_wait_bcon_tmout(struct ci_hdrc *ci) +{ + ci->fsm.a_wait_bcon_tmout = 1; + return 0; +} + +static int a_aidl_bdis_tmout(struct ci_hdrc *ci) +{ + ci->fsm.a_aidl_bdis_tmout = 1; + return 0; +} + +static int b_ase0_brst_tmout(struct ci_hdrc *ci) +{ + ci->fsm.b_ase0_brst_tmout = 1; + return 0; +} + +static int a_bidl_adis_tmout(struct ci_hdrc *ci) +{ + ci->fsm.a_bidl_adis_tmout = 1; + return 0; +} + +static int b_aidl_bdis_tmout(struct ci_hdrc *ci) +{ + ci->fsm.a_bus_suspend = 1; + return 0; +} + +static int b_se0_srp_tmout(struct ci_hdrc *ci) +{ + ci->fsm.b_se0_srp = 1; + return 0; +} + +static int b_srp_fail_tmout(struct ci_hdrc *ci) +{ + ci->fsm.b_srp_done = 1; + return 1; +} + +static int b_data_pls_tmout(struct ci_hdrc *ci) +{ + ci->fsm.b_srp_done = 1; + ci->fsm.b_bus_req = 0; + if (ci->fsm.power_up) + ci->fsm.power_up = 0; + hw_write_otgsc(ci, OTGSC_HABA, 0); + pm_runtime_put(ci->dev); + return 0; +} + +static int b_ssend_srp_tmout(struct ci_hdrc *ci) +{ + ci->fsm.b_ssend_srp = 1; + /* only vbus fall below B_sess_vld in b_idle state */ + if (ci->fsm.otg->state == OTG_STATE_B_IDLE) + return 0; + else + return 1; +} + +/* + * Keep this list in the same order as timers indexed + * by enum otg_fsm_timer in include/linux/usb/otg-fsm.h + */ +static int (*otg_timer_handlers[])(struct ci_hdrc *) = { + a_wait_vrise_tmout, /* A_WAIT_VRISE */ + a_wait_vfall_tmout, /* A_WAIT_VFALL */ + a_wait_bcon_tmout, /* A_WAIT_BCON */ + a_aidl_bdis_tmout, /* A_AIDL_BDIS */ + b_ase0_brst_tmout, /* B_ASE0_BRST */ + a_bidl_adis_tmout, /* A_BIDL_ADIS */ + b_aidl_bdis_tmout, /* B_AIDL_BDIS */ + b_se0_srp_tmout, /* B_SE0_SRP */ + b_srp_fail_tmout, /* B_SRP_FAIL */ + NULL, /* A_WAIT_ENUM */ + b_data_pls_tmout, /* B_DATA_PLS */ + b_ssend_srp_tmout, /* B_SSEND_SRP */ +}; + +/* + * Enable the next nearest enabled timer if have + */ +static enum hrtimer_restart ci_otg_hrtimer_func(struct hrtimer *t) +{ + struct ci_hdrc *ci = container_of(t, struct ci_hdrc, otg_fsm_hrtimer); + ktime_t now, *timeout; + unsigned long enabled_timer_bits; + unsigned long flags; + enum otg_fsm_timer cur_timer, next_timer = NUM_OTG_FSM_TIMERS; + int ret = -EINVAL; + + spin_lock_irqsave(&ci->lock, flags); + enabled_timer_bits = ci->enabled_otg_timer_bits; + ci->next_otg_timer = NUM_OTG_FSM_TIMERS; + + now = ktime_get(); + for_each_set_bit(cur_timer, &enabled_timer_bits, NUM_OTG_FSM_TIMERS) { + if (ktime_compare(now, ci->hr_timeouts[cur_timer]) >= 0) { + ci->enabled_otg_timer_bits &= ~(1 << cur_timer); + if (otg_timer_handlers[cur_timer]) + ret = otg_timer_handlers[cur_timer](ci); + } else { + if ((next_timer == NUM_OTG_FSM_TIMERS) || + ktime_before(ci->hr_timeouts[cur_timer], + ci->hr_timeouts[next_timer])) + next_timer = cur_timer; + } + } + /* Enable the next nearest timer */ + if (next_timer < NUM_OTG_FSM_TIMERS) { + timeout = &ci->hr_timeouts[next_timer]; + hrtimer_start_range_ns(&ci->otg_fsm_hrtimer, *timeout, + NSEC_PER_MSEC, HRTIMER_MODE_ABS); + ci->next_otg_timer = next_timer; + } + spin_unlock_irqrestore(&ci->lock, flags); + + if (!ret) + ci_otg_queue_work(ci); + + return HRTIMER_NORESTART; +} + +/* Initialize timers */ +static int ci_otg_init_timers(struct ci_hdrc *ci) +{ + hrtimer_init(&ci->otg_fsm_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS); + ci->otg_fsm_hrtimer.function = ci_otg_hrtimer_func; + + return 0; +} + +/* -------------------------------------------------------------*/ +/* Operations that will be called from OTG Finite State Machine */ +/* -------------------------------------------------------------*/ +static void ci_otg_fsm_add_timer(struct otg_fsm *fsm, enum otg_fsm_timer t) +{ + struct ci_hdrc *ci = container_of(fsm, struct ci_hdrc, fsm); + + if (t < NUM_OTG_FSM_TIMERS) + ci_otg_add_timer(ci, t); + return; +} + +static void ci_otg_fsm_del_timer(struct otg_fsm *fsm, enum otg_fsm_timer t) +{ + struct ci_hdrc *ci = container_of(fsm, struct ci_hdrc, fsm); + + if (t < NUM_OTG_FSM_TIMERS) + ci_otg_del_timer(ci, t); + return; +} + +/* + * A-device drive vbus: turn on vbus regulator and enable port power + * Data pulse irq should be disabled while vbus is on. + */ +static void ci_otg_drv_vbus(struct otg_fsm *fsm, int on) +{ + int ret; + struct ci_hdrc *ci = container_of(fsm, struct ci_hdrc, fsm); + + if (on) { + /* Enable power power */ + hw_write(ci, OP_PORTSC, PORTSC_W1C_BITS | PORTSC_PP, + PORTSC_PP); + if (ci->platdata->reg_vbus) { + ret = regulator_enable(ci->platdata->reg_vbus); + if (ret) { + dev_err(ci->dev, + "Failed to enable vbus regulator, ret=%d\n", + ret); + return; + } + } + /* Disable data pulse irq */ + hw_write_otgsc(ci, OTGSC_DPIE, 0); + + fsm->a_srp_det = 0; + fsm->power_up = 0; + } else { + if (ci->platdata->reg_vbus) + regulator_disable(ci->platdata->reg_vbus); + + fsm->a_bus_drop = 1; + fsm->a_bus_req = 0; + } +} + +/* + * Control data line by Run Stop bit. + */ +static void ci_otg_loc_conn(struct otg_fsm *fsm, int on) +{ + struct ci_hdrc *ci = container_of(fsm, struct ci_hdrc, fsm); + + if (on) + hw_write(ci, OP_USBCMD, USBCMD_RS, USBCMD_RS); + else + hw_write(ci, OP_USBCMD, USBCMD_RS, 0); +} + +/* + * Generate SOF by host. + * In host mode, controller will automatically send SOF. + * Suspend will block the data on the port. + * + * This is controlled through usbcore by usb autosuspend, + * so the usb device class driver need support autosuspend, + * otherwise the bus suspend will not happen. + */ +static void ci_otg_loc_sof(struct otg_fsm *fsm, int on) +{ + struct usb_device *udev; + + if (!fsm->otg->host) + return; + + udev = usb_hub_find_child(fsm->otg->host->root_hub, 1); + if (!udev) + return; + + if (on) { + usb_disable_autosuspend(udev); + } else { + pm_runtime_set_autosuspend_delay(&udev->dev, 0); + usb_enable_autosuspend(udev); + } +} + +/* + * Start SRP pulsing by data-line pulsing, + * no v-bus pulsing followed + */ +static void ci_otg_start_pulse(struct otg_fsm *fsm) +{ + struct ci_hdrc *ci = container_of(fsm, struct ci_hdrc, fsm); + + /* Hardware Assistant Data pulse */ + hw_write_otgsc(ci, OTGSC_HADP, OTGSC_HADP); + + pm_runtime_get(ci->dev); + ci_otg_add_timer(ci, B_DATA_PLS); +} + +static int ci_otg_start_host(struct otg_fsm *fsm, int on) +{ + struct ci_hdrc *ci = container_of(fsm, struct ci_hdrc, fsm); + + if (on) { + ci_role_stop(ci); + ci_role_start(ci, CI_ROLE_HOST); + } else { + ci_role_stop(ci); + ci_role_start(ci, CI_ROLE_GADGET); + } + return 0; +} + +static int ci_otg_start_gadget(struct otg_fsm *fsm, int on) +{ + struct ci_hdrc *ci = container_of(fsm, struct ci_hdrc, fsm); + + if (on) + usb_gadget_vbus_connect(&ci->gadget); + else + usb_gadget_vbus_disconnect(&ci->gadget); + + return 0; +} + +static struct otg_fsm_ops ci_otg_ops = { + .drv_vbus = ci_otg_drv_vbus, + .loc_conn = ci_otg_loc_conn, + .loc_sof = ci_otg_loc_sof, + .start_pulse = ci_otg_start_pulse, + .add_timer = ci_otg_fsm_add_timer, + .del_timer = ci_otg_fsm_del_timer, + .start_host = ci_otg_start_host, + .start_gadget = ci_otg_start_gadget, +}; + +int ci_otg_fsm_work(struct ci_hdrc *ci) +{ + /* + * Don't do fsm transition for B device + * when there is no gadget class driver + */ + if (ci->fsm.id && !(ci->driver) && + ci->fsm.otg->state < OTG_STATE_A_IDLE) + return 0; + + pm_runtime_get_sync(ci->dev); + if (otg_statemachine(&ci->fsm)) { + if (ci->fsm.otg->state == OTG_STATE_A_IDLE) { + /* + * Further state change for cases: + * a_idle to b_idle; or + * a_idle to a_wait_vrise due to ID change(1->0), so + * B-dev becomes A-dev can try to start new session + * consequently; or + * a_idle to a_wait_vrise when power up + */ + if ((ci->fsm.id) || (ci->id_event) || + (ci->fsm.power_up)) { + ci_otg_queue_work(ci); + } else { + /* Enable data pulse irq */ + hw_write(ci, OP_PORTSC, PORTSC_W1C_BITS | + PORTSC_PP, 0); + hw_write_otgsc(ci, OTGSC_DPIS, OTGSC_DPIS); + hw_write_otgsc(ci, OTGSC_DPIE, OTGSC_DPIE); + } + if (ci->id_event) + ci->id_event = false; + } else if (ci->fsm.otg->state == OTG_STATE_B_IDLE) { + if (ci->fsm.b_sess_vld) { + ci->fsm.power_up = 0; + /* + * Further transite to b_periphearl state + * when register gadget driver with vbus on + */ + ci_otg_queue_work(ci); + } + } else if (ci->fsm.otg->state == OTG_STATE_A_HOST) { + pm_runtime_mark_last_busy(ci->dev); + pm_runtime_put_autosuspend(ci->dev); + return 0; + } + } + pm_runtime_put_sync(ci->dev); + return 0; +} + +/* + * Update fsm variables in each state if catching expected interrupts, + * called by otg fsm isr. + */ +static void ci_otg_fsm_event(struct ci_hdrc *ci) +{ + u32 intr_sts, otg_bsess_vld, port_conn; + struct otg_fsm *fsm = &ci->fsm; + + intr_sts = hw_read_intr_status(ci); + otg_bsess_vld = hw_read_otgsc(ci, OTGSC_BSV); + port_conn = hw_read(ci, OP_PORTSC, PORTSC_CCS); + + switch (ci->fsm.otg->state) { + case OTG_STATE_A_WAIT_BCON: + if (port_conn) { + fsm->b_conn = 1; + fsm->a_bus_req = 1; + ci_otg_queue_work(ci); + } + break; + case OTG_STATE_B_IDLE: + if (otg_bsess_vld && (intr_sts & USBi_PCI) && port_conn) { + fsm->b_sess_vld = 1; + ci_otg_queue_work(ci); + } + break; + case OTG_STATE_B_PERIPHERAL: + if ((intr_sts & USBi_SLI) && port_conn && otg_bsess_vld) { + ci_otg_add_timer(ci, B_AIDL_BDIS); + } else if (intr_sts & USBi_PCI) { + ci_otg_del_timer(ci, B_AIDL_BDIS); + if (fsm->a_bus_suspend == 1) + fsm->a_bus_suspend = 0; + } + break; + case OTG_STATE_B_HOST: + if ((intr_sts & USBi_PCI) && !port_conn) { + fsm->a_conn = 0; + fsm->b_bus_req = 0; + ci_otg_queue_work(ci); + } + break; + case OTG_STATE_A_PERIPHERAL: + if (intr_sts & USBi_SLI) { + fsm->b_bus_suspend = 1; + /* + * Init a timer to know how long this suspend + * will continue, if time out, indicates B no longer + * wants to be host role + */ + ci_otg_add_timer(ci, A_BIDL_ADIS); + } + + if (intr_sts & USBi_URI) + ci_otg_del_timer(ci, A_BIDL_ADIS); + + if (intr_sts & USBi_PCI) { + if (fsm->b_bus_suspend == 1) { + ci_otg_del_timer(ci, A_BIDL_ADIS); + fsm->b_bus_suspend = 0; + } + } + break; + case OTG_STATE_A_SUSPEND: + if ((intr_sts & USBi_PCI) && !port_conn) { + fsm->b_conn = 0; + + /* if gadget driver is binded */ + if (ci->driver) { + /* A device to be peripheral mode */ + ci->gadget.is_a_peripheral = 1; + } + ci_otg_queue_work(ci); + } + break; + case OTG_STATE_A_HOST: + if ((intr_sts & USBi_PCI) && !port_conn) { + fsm->b_conn = 0; + ci_otg_queue_work(ci); + } + break; + case OTG_STATE_B_WAIT_ACON: + if ((intr_sts & USBi_PCI) && port_conn) { + fsm->a_conn = 1; + ci_otg_queue_work(ci); + } + break; + default: + break; + } +} + +/* + * ci_otg_irq - otg fsm related irq handling + * and also update otg fsm variable by monitoring usb host and udc + * state change interrupts. + * @ci: ci_hdrc + */ +irqreturn_t ci_otg_fsm_irq(struct ci_hdrc *ci) +{ + irqreturn_t retval = IRQ_NONE; + u32 otgsc, otg_int_src = 0; + struct otg_fsm *fsm = &ci->fsm; + + otgsc = hw_read_otgsc(ci, ~0); + otg_int_src = otgsc & OTGSC_INT_STATUS_BITS & (otgsc >> 8); + fsm->id = (otgsc & OTGSC_ID) ? 1 : 0; + + if (otg_int_src) { + if (otg_int_src & OTGSC_DPIS) { + hw_write_otgsc(ci, OTGSC_DPIS, OTGSC_DPIS); + fsm->a_srp_det = 1; + fsm->a_bus_drop = 0; + } else if (otg_int_src & OTGSC_IDIS) { + hw_write_otgsc(ci, OTGSC_IDIS, OTGSC_IDIS); + if (fsm->id == 0) { + fsm->a_bus_drop = 0; + fsm->a_bus_req = 1; + ci->id_event = true; + } + } else if (otg_int_src & OTGSC_BSVIS) { + hw_write_otgsc(ci, OTGSC_BSVIS, OTGSC_BSVIS); + if (otgsc & OTGSC_BSV) { + fsm->b_sess_vld = 1; + ci_otg_del_timer(ci, B_SSEND_SRP); + ci_otg_del_timer(ci, B_SRP_FAIL); + fsm->b_ssend_srp = 0; + } else { + fsm->b_sess_vld = 0; + if (fsm->id) + ci_otg_add_timer(ci, B_SSEND_SRP); + } + } else if (otg_int_src & OTGSC_AVVIS) { + hw_write_otgsc(ci, OTGSC_AVVIS, OTGSC_AVVIS); + if (otgsc & OTGSC_AVV) { + fsm->a_vbus_vld = 1; + } else { + fsm->a_vbus_vld = 0; + fsm->b_conn = 0; + } + } + ci_otg_queue_work(ci); + return IRQ_HANDLED; + } + + ci_otg_fsm_event(ci); + + return retval; +} + +void ci_hdrc_otg_fsm_start(struct ci_hdrc *ci) +{ + ci_otg_queue_work(ci); +} + +int ci_hdrc_otg_fsm_init(struct ci_hdrc *ci) +{ + int retval = 0; + + if (ci->phy) + ci->otg.phy = ci->phy; + else + ci->otg.usb_phy = ci->usb_phy; + + ci->otg.gadget = &ci->gadget; + ci->fsm.otg = &ci->otg; + ci->fsm.power_up = 1; + ci->fsm.id = hw_read_otgsc(ci, OTGSC_ID) ? 1 : 0; + ci->fsm.otg->state = OTG_STATE_UNDEFINED; + ci->fsm.ops = &ci_otg_ops; + ci->gadget.hnp_polling_support = 1; + ci->fsm.host_req_flag = devm_kzalloc(ci->dev, 1, GFP_KERNEL); + if (!ci->fsm.host_req_flag) + return -ENOMEM; + + mutex_init(&ci->fsm.lock); + + retval = ci_otg_init_timers(ci); + if (retval) { + dev_err(ci->dev, "Couldn't init OTG timers\n"); + return retval; + } + ci->enabled_otg_timer_bits = 0; + ci->next_otg_timer = NUM_OTG_FSM_TIMERS; + + retval = sysfs_create_group(&ci->dev->kobj, &inputs_attr_group); + if (retval < 0) { + dev_dbg(ci->dev, + "Can't register sysfs attr group: %d\n", retval); + return retval; + } + + /* Enable A vbus valid irq */ + hw_write_otgsc(ci, OTGSC_AVVIE, OTGSC_AVVIE); + + if (ci->fsm.id) { + ci->fsm.b_ssend_srp = + hw_read_otgsc(ci, OTGSC_BSV) ? 0 : 1; + ci->fsm.b_sess_vld = + hw_read_otgsc(ci, OTGSC_BSV) ? 1 : 0; + /* Enable BSV irq */ + hw_write_otgsc(ci, OTGSC_BSVIE, OTGSC_BSVIE); + } + + return 0; +} + +void ci_hdrc_otg_fsm_remove(struct ci_hdrc *ci) +{ + sysfs_remove_group(&ci->dev->kobj, &inputs_attr_group); +} diff --git a/drivers/usb/chipidea/otg_fsm.h b/drivers/usb/chipidea/otg_fsm.h new file mode 100644 index 000000000..2b49d29bf --- /dev/null +++ b/drivers/usb/chipidea/otg_fsm.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2014 Freescale Semiconductor, Inc. + * + * Author: Jun Li + */ + +#ifndef __DRIVERS_USB_CHIPIDEA_OTG_FSM_H +#define __DRIVERS_USB_CHIPIDEA_OTG_FSM_H + +#include <linux/usb/otg-fsm.h> + +/* + * A-DEVICE timing constants + */ + +/* Wait for VBUS Rise */ +#define TA_WAIT_VRISE (100) /* a_wait_vrise: section 7.1.2 + * a_wait_vrise_tmr: section 7.4.5.1 + * TA_VBUS_RISE <= 100ms, section 4.4 + * Table 4-1: Electrical Characteristics + * ->DC Electrical Timing + */ +/* Wait for VBUS Fall */ +#define TA_WAIT_VFALL (1000) /* a_wait_vfall: section 7.1.7 + * a_wait_vfall_tmr: section: 7.4.5.2 + */ +/* Wait for B-Connect */ +#define TA_WAIT_BCON (10000) /* a_wait_bcon: section 7.1.3 + * TA_WAIT_BCON: should be between 1100 + * and 30000 ms, section 5.5, Table 5-1 + */ +/* A-Idle to B-Disconnect */ +#define TA_AIDL_BDIS (5000) /* a_suspend min 200 ms, section 5.2.1 + * TA_AIDL_BDIS: section 5.5, Table 5-1 + */ +/* B-Idle to A-Disconnect */ +#define TA_BIDL_ADIS (500) /* TA_BIDL_ADIS: section 5.2.1 + * 500ms is used for B switch to host + * for safe + */ + +/* + * B-device timing constants + */ + +/* Data-Line Pulse Time*/ +#define TB_DATA_PLS (10) /* b_srp_init,continue 5~10ms + * section:5.1.3 + */ +/* SRP Fail Time */ +#define TB_SRP_FAIL (6000) /* b_srp_init,fail time 5~6s + * section:5.1.6 + */ +/* A-SE0 to B-Reset */ +#define TB_ASE0_BRST (155) /* minimum 155 ms, section:5.3.1 */ +/* SE0 Time Before SRP */ +#define TB_SE0_SRP (1000) /* b_idle,minimum 1s, section:5.1.2 */ +/* SSEND time before SRP */ +#define TB_SSEND_SRP (1500) /* minimum 1.5 sec, section:5.1.2 */ + +#define TB_AIDL_BDIS (20) /* 4ms ~ 150ms, section 5.2.1 */ + +#if IS_ENABLED(CONFIG_USB_OTG_FSM) + +int ci_hdrc_otg_fsm_init(struct ci_hdrc *ci); +int ci_otg_fsm_work(struct ci_hdrc *ci); +irqreturn_t ci_otg_fsm_irq(struct ci_hdrc *ci); +void ci_hdrc_otg_fsm_start(struct ci_hdrc *ci); +void ci_hdrc_otg_fsm_remove(struct ci_hdrc *ci); + +#else + +static inline int ci_hdrc_otg_fsm_init(struct ci_hdrc *ci) +{ + return 0; +} + +static inline int ci_otg_fsm_work(struct ci_hdrc *ci) +{ + return -ENXIO; +} + +static inline irqreturn_t ci_otg_fsm_irq(struct ci_hdrc *ci) +{ + return IRQ_NONE; +} + +static inline void ci_hdrc_otg_fsm_start(struct ci_hdrc *ci) +{ + +} + +static inline void ci_hdrc_otg_fsm_remove(struct ci_hdrc *ci) +{ + +} + +#endif + +#endif /* __DRIVERS_USB_CHIPIDEA_OTG_FSM_H */ diff --git a/drivers/usb/chipidea/udc.c b/drivers/usb/chipidea/udc.c new file mode 100644 index 000000000..169ccfacf --- /dev/null +++ b/drivers/usb/chipidea/udc.c @@ -0,0 +1,2052 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * udc.c - ChipIdea UDC driver + * + * Copyright (C) 2008 Chipidea - MIPS Technologies, Inc. All rights reserved. + * + * Author: David Lopo + */ + +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/dmapool.h> +#include <linux/err.h> +#include <linux/irqreturn.h> +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/pm_runtime.h> +#include <linux/usb/ch9.h> +#include <linux/usb/gadget.h> +#include <linux/usb/otg-fsm.h> +#include <linux/usb/chipidea.h> + +#include "ci.h" +#include "udc.h" +#include "bits.h" +#include "otg.h" +#include "otg_fsm.h" + +/* control endpoint description */ +static const struct usb_endpoint_descriptor +ctrl_endpt_out_desc = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + + .bEndpointAddress = USB_DIR_OUT, + .bmAttributes = USB_ENDPOINT_XFER_CONTROL, + .wMaxPacketSize = cpu_to_le16(CTRL_PAYLOAD_MAX), +}; + +static const struct usb_endpoint_descriptor +ctrl_endpt_in_desc = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + + .bEndpointAddress = USB_DIR_IN, + .bmAttributes = USB_ENDPOINT_XFER_CONTROL, + .wMaxPacketSize = cpu_to_le16(CTRL_PAYLOAD_MAX), +}; + +/** + * hw_ep_bit: calculates the bit number + * @num: endpoint number + * @dir: endpoint direction + * + * This function returns bit number + */ +static inline int hw_ep_bit(int num, int dir) +{ + return num + ((dir == TX) ? 16 : 0); +} + +static inline int ep_to_bit(struct ci_hdrc *ci, int n) +{ + int fill = 16 - ci->hw_ep_max / 2; + + if (n >= ci->hw_ep_max / 2) + n += fill; + + return n; +} + +/** + * hw_device_state: enables/disables interrupts (execute without interruption) + * @dma: 0 => disable, !0 => enable and set dma engine + * + * This function returns an error code + */ +static int hw_device_state(struct ci_hdrc *ci, u32 dma) +{ + if (dma) { + hw_write(ci, OP_ENDPTLISTADDR, ~0, dma); + /* interrupt, error, port change, reset, sleep/suspend */ + hw_write(ci, OP_USBINTR, ~0, + USBi_UI|USBi_UEI|USBi_PCI|USBi_URI|USBi_SLI); + } else { + hw_write(ci, OP_USBINTR, ~0, 0); + } + return 0; +} + +/** + * hw_ep_flush: flush endpoint fifo (execute without interruption) + * @num: endpoint number + * @dir: endpoint direction + * + * This function returns an error code + */ +static int hw_ep_flush(struct ci_hdrc *ci, int num, int dir) +{ + int n = hw_ep_bit(num, dir); + + do { + /* flush any pending transfer */ + hw_write(ci, OP_ENDPTFLUSH, ~0, BIT(n)); + while (hw_read(ci, OP_ENDPTFLUSH, BIT(n))) + cpu_relax(); + } while (hw_read(ci, OP_ENDPTSTAT, BIT(n))); + + return 0; +} + +/** + * hw_ep_disable: disables endpoint (execute without interruption) + * @num: endpoint number + * @dir: endpoint direction + * + * This function returns an error code + */ +static int hw_ep_disable(struct ci_hdrc *ci, int num, int dir) +{ + hw_write(ci, OP_ENDPTCTRL + num, + (dir == TX) ? ENDPTCTRL_TXE : ENDPTCTRL_RXE, 0); + return 0; +} + +/** + * hw_ep_enable: enables endpoint (execute without interruption) + * @num: endpoint number + * @dir: endpoint direction + * @type: endpoint type + * + * This function returns an error code + */ +static int hw_ep_enable(struct ci_hdrc *ci, int num, int dir, int type) +{ + u32 mask, data; + + if (dir == TX) { + mask = ENDPTCTRL_TXT; /* type */ + data = type << __ffs(mask); + + mask |= ENDPTCTRL_TXS; /* unstall */ + mask |= ENDPTCTRL_TXR; /* reset data toggle */ + data |= ENDPTCTRL_TXR; + mask |= ENDPTCTRL_TXE; /* enable */ + data |= ENDPTCTRL_TXE; + } else { + mask = ENDPTCTRL_RXT; /* type */ + data = type << __ffs(mask); + + mask |= ENDPTCTRL_RXS; /* unstall */ + mask |= ENDPTCTRL_RXR; /* reset data toggle */ + data |= ENDPTCTRL_RXR; + mask |= ENDPTCTRL_RXE; /* enable */ + data |= ENDPTCTRL_RXE; + } + hw_write(ci, OP_ENDPTCTRL + num, mask, data); + return 0; +} + +/** + * hw_ep_get_halt: return endpoint halt status + * @num: endpoint number + * @dir: endpoint direction + * + * This function returns 1 if endpoint halted + */ +static int hw_ep_get_halt(struct ci_hdrc *ci, int num, int dir) +{ + u32 mask = (dir == TX) ? ENDPTCTRL_TXS : ENDPTCTRL_RXS; + + return hw_read(ci, OP_ENDPTCTRL + num, mask) ? 1 : 0; +} + +/** + * hw_ep_prime: primes endpoint (execute without interruption) + * @num: endpoint number + * @dir: endpoint direction + * @is_ctrl: true if control endpoint + * + * This function returns an error code + */ +static int hw_ep_prime(struct ci_hdrc *ci, int num, int dir, int is_ctrl) +{ + int n = hw_ep_bit(num, dir); + + /* Synchronize before ep prime */ + wmb(); + + if (is_ctrl && dir == RX && hw_read(ci, OP_ENDPTSETUPSTAT, BIT(num))) + return -EAGAIN; + + hw_write(ci, OP_ENDPTPRIME, ~0, BIT(n)); + + while (hw_read(ci, OP_ENDPTPRIME, BIT(n))) + cpu_relax(); + if (is_ctrl && dir == RX && hw_read(ci, OP_ENDPTSETUPSTAT, BIT(num))) + return -EAGAIN; + + /* status shoult be tested according with manual but it doesn't work */ + return 0; +} + +/** + * hw_ep_set_halt: configures ep halt & resets data toggle after clear (execute + * without interruption) + * @num: endpoint number + * @dir: endpoint direction + * @value: true => stall, false => unstall + * + * This function returns an error code + */ +static int hw_ep_set_halt(struct ci_hdrc *ci, int num, int dir, int value) +{ + if (value != 0 && value != 1) + return -EINVAL; + + do { + enum ci_hw_regs reg = OP_ENDPTCTRL + num; + u32 mask_xs = (dir == TX) ? ENDPTCTRL_TXS : ENDPTCTRL_RXS; + u32 mask_xr = (dir == TX) ? ENDPTCTRL_TXR : ENDPTCTRL_RXR; + + /* data toggle - reserved for EP0 but it's in ESS */ + hw_write(ci, reg, mask_xs|mask_xr, + value ? mask_xs : mask_xr); + } while (value != hw_ep_get_halt(ci, num, dir)); + + return 0; +} + +/** + * hw_is_port_high_speed: test if port is high speed + * + * This function returns true if high speed port + */ +static int hw_port_is_high_speed(struct ci_hdrc *ci) +{ + return ci->hw_bank.lpm ? hw_read(ci, OP_DEVLC, DEVLC_PSPD) : + hw_read(ci, OP_PORTSC, PORTSC_HSP); +} + +/** + * hw_test_and_clear_complete: test & clear complete status (execute without + * interruption) + * @n: endpoint number + * + * This function returns complete status + */ +static int hw_test_and_clear_complete(struct ci_hdrc *ci, int n) +{ + n = ep_to_bit(ci, n); + return hw_test_and_clear(ci, OP_ENDPTCOMPLETE, BIT(n)); +} + +/** + * hw_test_and_clear_intr_active: test & clear active interrupts (execute + * without interruption) + * + * This function returns active interrutps + */ +static u32 hw_test_and_clear_intr_active(struct ci_hdrc *ci) +{ + u32 reg = hw_read_intr_status(ci) & hw_read_intr_enable(ci); + + hw_write(ci, OP_USBSTS, ~0, reg); + return reg; +} + +/** + * hw_test_and_clear_setup_guard: test & clear setup guard (execute without + * interruption) + * + * This function returns guard value + */ +static int hw_test_and_clear_setup_guard(struct ci_hdrc *ci) +{ + return hw_test_and_write(ci, OP_USBCMD, USBCMD_SUTW, 0); +} + +/** + * hw_test_and_set_setup_guard: test & set setup guard (execute without + * interruption) + * + * This function returns guard value + */ +static int hw_test_and_set_setup_guard(struct ci_hdrc *ci) +{ + return hw_test_and_write(ci, OP_USBCMD, USBCMD_SUTW, USBCMD_SUTW); +} + +/** + * hw_usb_set_address: configures USB address (execute without interruption) + * @value: new USB address + * + * This function explicitly sets the address, without the "USBADRA" (advance) + * feature, which is not supported by older versions of the controller. + */ +static void hw_usb_set_address(struct ci_hdrc *ci, u8 value) +{ + hw_write(ci, OP_DEVICEADDR, DEVICEADDR_USBADR, + value << __ffs(DEVICEADDR_USBADR)); +} + +/** + * hw_usb_reset: restart device after a bus reset (execute without + * interruption) + * + * This function returns an error code + */ +static int hw_usb_reset(struct ci_hdrc *ci) +{ + hw_usb_set_address(ci, 0); + + /* ESS flushes only at end?!? */ + hw_write(ci, OP_ENDPTFLUSH, ~0, ~0); + + /* clear setup token semaphores */ + hw_write(ci, OP_ENDPTSETUPSTAT, 0, 0); + + /* clear complete status */ + hw_write(ci, OP_ENDPTCOMPLETE, 0, 0); + + /* wait until all bits cleared */ + while (hw_read(ci, OP_ENDPTPRIME, ~0)) + udelay(10); /* not RTOS friendly */ + + /* reset all endpoints ? */ + + /* reset internal status and wait for further instructions + no need to verify the port reset status (ESS does it) */ + + return 0; +} + +/****************************************************************************** + * UTIL block + *****************************************************************************/ + +static int add_td_to_list(struct ci_hw_ep *hwep, struct ci_hw_req *hwreq, + unsigned length) +{ + int i; + u32 temp; + struct td_node *lastnode, *node = kzalloc(sizeof(struct td_node), + GFP_ATOMIC); + + if (node == NULL) + return -ENOMEM; + + node->ptr = dma_pool_zalloc(hwep->td_pool, GFP_ATOMIC, &node->dma); + if (node->ptr == NULL) { + kfree(node); + return -ENOMEM; + } + + node->ptr->token = cpu_to_le32(length << __ffs(TD_TOTAL_BYTES)); + node->ptr->token &= cpu_to_le32(TD_TOTAL_BYTES); + node->ptr->token |= cpu_to_le32(TD_STATUS_ACTIVE); + if (hwep->type == USB_ENDPOINT_XFER_ISOC && hwep->dir == TX) { + u32 mul = hwreq->req.length / hwep->ep.maxpacket; + + if (hwreq->req.length == 0 + || hwreq->req.length % hwep->ep.maxpacket) + mul++; + node->ptr->token |= cpu_to_le32(mul << __ffs(TD_MULTO)); + } + + temp = (u32) (hwreq->req.dma + hwreq->req.actual); + if (length) { + node->ptr->page[0] = cpu_to_le32(temp); + for (i = 1; i < TD_PAGE_COUNT; i++) { + u32 page = temp + i * CI_HDRC_PAGE_SIZE; + page &= ~TD_RESERVED_MASK; + node->ptr->page[i] = cpu_to_le32(page); + } + } + + hwreq->req.actual += length; + + if (!list_empty(&hwreq->tds)) { + /* get the last entry */ + lastnode = list_entry(hwreq->tds.prev, + struct td_node, td); + lastnode->ptr->next = cpu_to_le32(node->dma); + } + + INIT_LIST_HEAD(&node->td); + list_add_tail(&node->td, &hwreq->tds); + + return 0; +} + +/** + * _usb_addr: calculates endpoint address from direction & number + * @ep: endpoint + */ +static inline u8 _usb_addr(struct ci_hw_ep *ep) +{ + return ((ep->dir == TX) ? USB_ENDPOINT_DIR_MASK : 0) | ep->num; +} + +/** + * _hardware_enqueue: configures a request at hardware level + * @hwep: endpoint + * @hwreq: request + * + * This function returns an error code + */ +static int _hardware_enqueue(struct ci_hw_ep *hwep, struct ci_hw_req *hwreq) +{ + struct ci_hdrc *ci = hwep->ci; + int ret = 0; + unsigned rest = hwreq->req.length; + int pages = TD_PAGE_COUNT; + struct td_node *firstnode, *lastnode; + + /* don't queue twice */ + if (hwreq->req.status == -EALREADY) + return -EALREADY; + + hwreq->req.status = -EALREADY; + + ret = usb_gadget_map_request_by_dev(ci->dev->parent, + &hwreq->req, hwep->dir); + if (ret) + return ret; + + /* + * The first buffer could be not page aligned. + * In that case we have to span into one extra td. + */ + if (hwreq->req.dma % PAGE_SIZE) + pages--; + + if (rest == 0) { + ret = add_td_to_list(hwep, hwreq, 0); + if (ret < 0) + goto done; + } + + while (rest > 0) { + unsigned count = min(hwreq->req.length - hwreq->req.actual, + (unsigned)(pages * CI_HDRC_PAGE_SIZE)); + ret = add_td_to_list(hwep, hwreq, count); + if (ret < 0) + goto done; + + rest -= count; + } + + if (hwreq->req.zero && hwreq->req.length && hwep->dir == TX + && (hwreq->req.length % hwep->ep.maxpacket == 0)) { + ret = add_td_to_list(hwep, hwreq, 0); + if (ret < 0) + goto done; + } + + firstnode = list_first_entry(&hwreq->tds, struct td_node, td); + + lastnode = list_entry(hwreq->tds.prev, + struct td_node, td); + + lastnode->ptr->next = cpu_to_le32(TD_TERMINATE); + if (!hwreq->req.no_interrupt) + lastnode->ptr->token |= cpu_to_le32(TD_IOC); + wmb(); + + hwreq->req.actual = 0; + if (!list_empty(&hwep->qh.queue)) { + struct ci_hw_req *hwreqprev; + int n = hw_ep_bit(hwep->num, hwep->dir); + int tmp_stat; + struct td_node *prevlastnode; + u32 next = firstnode->dma & TD_ADDR_MASK; + + hwreqprev = list_entry(hwep->qh.queue.prev, + struct ci_hw_req, queue); + prevlastnode = list_entry(hwreqprev->tds.prev, + struct td_node, td); + + prevlastnode->ptr->next = cpu_to_le32(next); + wmb(); + if (hw_read(ci, OP_ENDPTPRIME, BIT(n))) + goto done; + do { + hw_write(ci, OP_USBCMD, USBCMD_ATDTW, USBCMD_ATDTW); + tmp_stat = hw_read(ci, OP_ENDPTSTAT, BIT(n)); + } while (!hw_read(ci, OP_USBCMD, USBCMD_ATDTW)); + hw_write(ci, OP_USBCMD, USBCMD_ATDTW, 0); + if (tmp_stat) + goto done; + } + + /* QH configuration */ + hwep->qh.ptr->td.next = cpu_to_le32(firstnode->dma); + hwep->qh.ptr->td.token &= + cpu_to_le32(~(TD_STATUS_HALTED|TD_STATUS_ACTIVE)); + + if (hwep->type == USB_ENDPOINT_XFER_ISOC && hwep->dir == RX) { + u32 mul = hwreq->req.length / hwep->ep.maxpacket; + + if (hwreq->req.length == 0 + || hwreq->req.length % hwep->ep.maxpacket) + mul++; + hwep->qh.ptr->cap |= cpu_to_le32(mul << __ffs(QH_MULT)); + } + + ret = hw_ep_prime(ci, hwep->num, hwep->dir, + hwep->type == USB_ENDPOINT_XFER_CONTROL); +done: + return ret; +} + +/* + * free_pending_td: remove a pending request for the endpoint + * @hwep: endpoint + */ +static void free_pending_td(struct ci_hw_ep *hwep) +{ + struct td_node *pending = hwep->pending_td; + + dma_pool_free(hwep->td_pool, pending->ptr, pending->dma); + hwep->pending_td = NULL; + kfree(pending); +} + +static int reprime_dtd(struct ci_hdrc *ci, struct ci_hw_ep *hwep, + struct td_node *node) +{ + hwep->qh.ptr->td.next = cpu_to_le32(node->dma); + hwep->qh.ptr->td.token &= + cpu_to_le32(~(TD_STATUS_HALTED | TD_STATUS_ACTIVE)); + + return hw_ep_prime(ci, hwep->num, hwep->dir, + hwep->type == USB_ENDPOINT_XFER_CONTROL); +} + +/** + * _hardware_dequeue: handles a request at hardware level + * @gadget: gadget + * @hwep: endpoint + * + * This function returns an error code + */ +static int _hardware_dequeue(struct ci_hw_ep *hwep, struct ci_hw_req *hwreq) +{ + u32 tmptoken; + struct td_node *node, *tmpnode; + unsigned remaining_length; + unsigned actual = hwreq->req.length; + struct ci_hdrc *ci = hwep->ci; + + if (hwreq->req.status != -EALREADY) + return -EINVAL; + + hwreq->req.status = 0; + + list_for_each_entry_safe(node, tmpnode, &hwreq->tds, td) { + tmptoken = le32_to_cpu(node->ptr->token); + if ((TD_STATUS_ACTIVE & tmptoken) != 0) { + int n = hw_ep_bit(hwep->num, hwep->dir); + + if (ci->rev == CI_REVISION_24) + if (!hw_read(ci, OP_ENDPTSTAT, BIT(n))) + reprime_dtd(ci, hwep, node); + hwreq->req.status = -EALREADY; + return -EBUSY; + } + + remaining_length = (tmptoken & TD_TOTAL_BYTES); + remaining_length >>= __ffs(TD_TOTAL_BYTES); + actual -= remaining_length; + + hwreq->req.status = tmptoken & TD_STATUS; + if ((TD_STATUS_HALTED & hwreq->req.status)) { + hwreq->req.status = -EPIPE; + break; + } else if ((TD_STATUS_DT_ERR & hwreq->req.status)) { + hwreq->req.status = -EPROTO; + break; + } else if ((TD_STATUS_TR_ERR & hwreq->req.status)) { + hwreq->req.status = -EILSEQ; + break; + } + + if (remaining_length) { + if (hwep->dir == TX) { + hwreq->req.status = -EPROTO; + break; + } + } + /* + * As the hardware could still address the freed td + * which will run the udc unusable, the cleanup of the + * td has to be delayed by one. + */ + if (hwep->pending_td) + free_pending_td(hwep); + + hwep->pending_td = node; + list_del_init(&node->td); + } + + usb_gadget_unmap_request_by_dev(hwep->ci->dev->parent, + &hwreq->req, hwep->dir); + + hwreq->req.actual += actual; + + if (hwreq->req.status) + return hwreq->req.status; + + return hwreq->req.actual; +} + +/** + * _ep_nuke: dequeues all endpoint requests + * @hwep: endpoint + * + * This function returns an error code + * Caller must hold lock + */ +static int _ep_nuke(struct ci_hw_ep *hwep) +__releases(hwep->lock) +__acquires(hwep->lock) +{ + struct td_node *node, *tmpnode; + if (hwep == NULL) + return -EINVAL; + + hw_ep_flush(hwep->ci, hwep->num, hwep->dir); + + while (!list_empty(&hwep->qh.queue)) { + + /* pop oldest request */ + struct ci_hw_req *hwreq = list_entry(hwep->qh.queue.next, + struct ci_hw_req, queue); + + list_for_each_entry_safe(node, tmpnode, &hwreq->tds, td) { + dma_pool_free(hwep->td_pool, node->ptr, node->dma); + list_del_init(&node->td); + node->ptr = NULL; + kfree(node); + } + + list_del_init(&hwreq->queue); + hwreq->req.status = -ESHUTDOWN; + + if (hwreq->req.complete != NULL) { + spin_unlock(hwep->lock); + usb_gadget_giveback_request(&hwep->ep, &hwreq->req); + spin_lock(hwep->lock); + } + } + + if (hwep->pending_td) + free_pending_td(hwep); + + return 0; +} + +static int _ep_set_halt(struct usb_ep *ep, int value, bool check_transfer) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + int direction, retval = 0; + unsigned long flags; + + if (ep == NULL || hwep->ep.desc == NULL) + return -EINVAL; + + if (usb_endpoint_xfer_isoc(hwep->ep.desc)) + return -EOPNOTSUPP; + + spin_lock_irqsave(hwep->lock, flags); + + if (value && hwep->dir == TX && check_transfer && + !list_empty(&hwep->qh.queue) && + !usb_endpoint_xfer_control(hwep->ep.desc)) { + spin_unlock_irqrestore(hwep->lock, flags); + return -EAGAIN; + } + + direction = hwep->dir; + do { + retval |= hw_ep_set_halt(hwep->ci, hwep->num, hwep->dir, value); + + if (!value) + hwep->wedge = 0; + + if (hwep->type == USB_ENDPOINT_XFER_CONTROL) + hwep->dir = (hwep->dir == TX) ? RX : TX; + + } while (hwep->dir != direction); + + spin_unlock_irqrestore(hwep->lock, flags); + return retval; +} + + +/** + * _gadget_stop_activity: stops all USB activity, flushes & disables all endpts + * @gadget: gadget + * + * This function returns an error code + */ +static int _gadget_stop_activity(struct usb_gadget *gadget) +{ + struct usb_ep *ep; + struct ci_hdrc *ci = container_of(gadget, struct ci_hdrc, gadget); + unsigned long flags; + + /* flush all endpoints */ + gadget_for_each_ep(ep, gadget) { + usb_ep_fifo_flush(ep); + } + usb_ep_fifo_flush(&ci->ep0out->ep); + usb_ep_fifo_flush(&ci->ep0in->ep); + + /* make sure to disable all endpoints */ + gadget_for_each_ep(ep, gadget) { + usb_ep_disable(ep); + } + + if (ci->status != NULL) { + usb_ep_free_request(&ci->ep0in->ep, ci->status); + ci->status = NULL; + } + + spin_lock_irqsave(&ci->lock, flags); + ci->gadget.speed = USB_SPEED_UNKNOWN; + ci->remote_wakeup = 0; + ci->suspended = 0; + spin_unlock_irqrestore(&ci->lock, flags); + + return 0; +} + +/****************************************************************************** + * ISR block + *****************************************************************************/ +/** + * isr_reset_handler: USB reset interrupt handler + * @ci: UDC device + * + * This function resets USB engine after a bus reset occurred + */ +static void isr_reset_handler(struct ci_hdrc *ci) +__releases(ci->lock) +__acquires(ci->lock) +{ + int retval; + + spin_unlock(&ci->lock); + if (ci->gadget.speed != USB_SPEED_UNKNOWN) + usb_gadget_udc_reset(&ci->gadget, ci->driver); + + retval = _gadget_stop_activity(&ci->gadget); + if (retval) + goto done; + + retval = hw_usb_reset(ci); + if (retval) + goto done; + + ci->status = usb_ep_alloc_request(&ci->ep0in->ep, GFP_ATOMIC); + if (ci->status == NULL) + retval = -ENOMEM; + +done: + spin_lock(&ci->lock); + + if (retval) + dev_err(ci->dev, "error: %i\n", retval); +} + +/** + * isr_get_status_complete: get_status request complete function + * @ep: endpoint + * @req: request handled + * + * Caller must release lock + */ +static void isr_get_status_complete(struct usb_ep *ep, struct usb_request *req) +{ + if (ep == NULL || req == NULL) + return; + + kfree(req->buf); + usb_ep_free_request(ep, req); +} + +/** + * _ep_queue: queues (submits) an I/O request to an endpoint + * @ep: endpoint + * @req: request + * @gfp_flags: GFP flags (not used) + * + * Caller must hold lock + * This function returns an error code + */ +static int _ep_queue(struct usb_ep *ep, struct usb_request *req, + gfp_t __maybe_unused gfp_flags) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + struct ci_hw_req *hwreq = container_of(req, struct ci_hw_req, req); + struct ci_hdrc *ci = hwep->ci; + int retval = 0; + + if (ep == NULL || req == NULL || hwep->ep.desc == NULL) + return -EINVAL; + + if (hwep->type == USB_ENDPOINT_XFER_CONTROL) { + if (req->length) + hwep = (ci->ep0_dir == RX) ? + ci->ep0out : ci->ep0in; + if (!list_empty(&hwep->qh.queue)) { + _ep_nuke(hwep); + dev_warn(hwep->ci->dev, "endpoint ctrl %X nuked\n", + _usb_addr(hwep)); + } + } + + if (usb_endpoint_xfer_isoc(hwep->ep.desc) && + hwreq->req.length > hwep->ep.mult * hwep->ep.maxpacket) { + dev_err(hwep->ci->dev, "request length too big for isochronous\n"); + return -EMSGSIZE; + } + + /* first nuke then test link, e.g. previous status has not sent */ + if (!list_empty(&hwreq->queue)) { + dev_err(hwep->ci->dev, "request already in queue\n"); + return -EBUSY; + } + + /* push request */ + hwreq->req.status = -EINPROGRESS; + hwreq->req.actual = 0; + + retval = _hardware_enqueue(hwep, hwreq); + + if (retval == -EALREADY) + retval = 0; + if (!retval) + list_add_tail(&hwreq->queue, &hwep->qh.queue); + + return retval; +} + +/** + * isr_get_status_response: get_status request response + * @ci: ci struct + * @setup: setup request packet + * + * This function returns an error code + */ +static int isr_get_status_response(struct ci_hdrc *ci, + struct usb_ctrlrequest *setup) +__releases(hwep->lock) +__acquires(hwep->lock) +{ + struct ci_hw_ep *hwep = ci->ep0in; + struct usb_request *req = NULL; + gfp_t gfp_flags = GFP_ATOMIC; + int dir, num, retval; + + if (hwep == NULL || setup == NULL) + return -EINVAL; + + spin_unlock(hwep->lock); + req = usb_ep_alloc_request(&hwep->ep, gfp_flags); + spin_lock(hwep->lock); + if (req == NULL) + return -ENOMEM; + + req->complete = isr_get_status_complete; + req->length = 2; + req->buf = kzalloc(req->length, gfp_flags); + if (req->buf == NULL) { + retval = -ENOMEM; + goto err_free_req; + } + + if ((setup->bRequestType & USB_RECIP_MASK) == USB_RECIP_DEVICE) { + *(u16 *)req->buf = (ci->remote_wakeup << 1) | + ci->gadget.is_selfpowered; + } else if ((setup->bRequestType & USB_RECIP_MASK) \ + == USB_RECIP_ENDPOINT) { + dir = (le16_to_cpu(setup->wIndex) & USB_ENDPOINT_DIR_MASK) ? + TX : RX; + num = le16_to_cpu(setup->wIndex) & USB_ENDPOINT_NUMBER_MASK; + *(u16 *)req->buf = hw_ep_get_halt(ci, num, dir); + } + /* else do nothing; reserved for future use */ + + retval = _ep_queue(&hwep->ep, req, gfp_flags); + if (retval) + goto err_free_buf; + + return 0; + + err_free_buf: + kfree(req->buf); + err_free_req: + spin_unlock(hwep->lock); + usb_ep_free_request(&hwep->ep, req); + spin_lock(hwep->lock); + return retval; +} + +/** + * isr_setup_status_complete: setup_status request complete function + * @ep: endpoint + * @req: request handled + * + * Caller must release lock. Put the port in test mode if test mode + * feature is selected. + */ +static void +isr_setup_status_complete(struct usb_ep *ep, struct usb_request *req) +{ + struct ci_hdrc *ci = req->context; + unsigned long flags; + + if (ci->setaddr) { + hw_usb_set_address(ci, ci->address); + ci->setaddr = false; + if (ci->address) + usb_gadget_set_state(&ci->gadget, USB_STATE_ADDRESS); + } + + spin_lock_irqsave(&ci->lock, flags); + if (ci->test_mode) + hw_port_test_set(ci, ci->test_mode); + spin_unlock_irqrestore(&ci->lock, flags); +} + +/** + * isr_setup_status_phase: queues the status phase of a setup transation + * @ci: ci struct + * + * This function returns an error code + */ +static int isr_setup_status_phase(struct ci_hdrc *ci) +{ + struct ci_hw_ep *hwep; + + /* + * Unexpected USB controller behavior, caused by bad signal integrity + * or ground reference problems, can lead to isr_setup_status_phase + * being called with ci->status equal to NULL. + * If this situation occurs, you should review your USB hardware design. + */ + if (WARN_ON_ONCE(!ci->status)) + return -EPIPE; + + hwep = (ci->ep0_dir == TX) ? ci->ep0out : ci->ep0in; + ci->status->context = ci; + ci->status->complete = isr_setup_status_complete; + + return _ep_queue(&hwep->ep, ci->status, GFP_ATOMIC); +} + +/** + * isr_tr_complete_low: transaction complete low level handler + * @hwep: endpoint + * + * This function returns an error code + * Caller must hold lock + */ +static int isr_tr_complete_low(struct ci_hw_ep *hwep) +__releases(hwep->lock) +__acquires(hwep->lock) +{ + struct ci_hw_req *hwreq, *hwreqtemp; + struct ci_hw_ep *hweptemp = hwep; + int retval = 0; + + list_for_each_entry_safe(hwreq, hwreqtemp, &hwep->qh.queue, + queue) { + retval = _hardware_dequeue(hwep, hwreq); + if (retval < 0) + break; + list_del_init(&hwreq->queue); + if (hwreq->req.complete != NULL) { + spin_unlock(hwep->lock); + if ((hwep->type == USB_ENDPOINT_XFER_CONTROL) && + hwreq->req.length) + hweptemp = hwep->ci->ep0in; + usb_gadget_giveback_request(&hweptemp->ep, &hwreq->req); + spin_lock(hwep->lock); + } + } + + if (retval == -EBUSY) + retval = 0; + + return retval; +} + +static int otg_a_alt_hnp_support(struct ci_hdrc *ci) +{ + dev_warn(&ci->gadget.dev, + "connect the device to an alternate port if you want HNP\n"); + return isr_setup_status_phase(ci); +} + +/** + * isr_setup_packet_handler: setup packet handler + * @ci: UDC descriptor + * + * This function handles setup packet + */ +static void isr_setup_packet_handler(struct ci_hdrc *ci) +__releases(ci->lock) +__acquires(ci->lock) +{ + struct ci_hw_ep *hwep = &ci->ci_hw_ep[0]; + struct usb_ctrlrequest req; + int type, num, dir, err = -EINVAL; + u8 tmode = 0; + + /* + * Flush data and handshake transactions of previous + * setup packet. + */ + _ep_nuke(ci->ep0out); + _ep_nuke(ci->ep0in); + + /* read_setup_packet */ + do { + hw_test_and_set_setup_guard(ci); + memcpy(&req, &hwep->qh.ptr->setup, sizeof(req)); + } while (!hw_test_and_clear_setup_guard(ci)); + + type = req.bRequestType; + + ci->ep0_dir = (type & USB_DIR_IN) ? TX : RX; + + switch (req.bRequest) { + case USB_REQ_CLEAR_FEATURE: + if (type == (USB_DIR_OUT|USB_RECIP_ENDPOINT) && + le16_to_cpu(req.wValue) == + USB_ENDPOINT_HALT) { + if (req.wLength != 0) + break; + num = le16_to_cpu(req.wIndex); + dir = (num & USB_ENDPOINT_DIR_MASK) ? TX : RX; + num &= USB_ENDPOINT_NUMBER_MASK; + if (dir == TX) + num += ci->hw_ep_max / 2; + if (!ci->ci_hw_ep[num].wedge) { + spin_unlock(&ci->lock); + err = usb_ep_clear_halt( + &ci->ci_hw_ep[num].ep); + spin_lock(&ci->lock); + if (err) + break; + } + err = isr_setup_status_phase(ci); + } else if (type == (USB_DIR_OUT|USB_RECIP_DEVICE) && + le16_to_cpu(req.wValue) == + USB_DEVICE_REMOTE_WAKEUP) { + if (req.wLength != 0) + break; + ci->remote_wakeup = 0; + err = isr_setup_status_phase(ci); + } else { + goto delegate; + } + break; + case USB_REQ_GET_STATUS: + if ((type != (USB_DIR_IN|USB_RECIP_DEVICE) || + le16_to_cpu(req.wIndex) == OTG_STS_SELECTOR) && + type != (USB_DIR_IN|USB_RECIP_ENDPOINT) && + type != (USB_DIR_IN|USB_RECIP_INTERFACE)) + goto delegate; + if (le16_to_cpu(req.wLength) != 2 || + le16_to_cpu(req.wValue) != 0) + break; + err = isr_get_status_response(ci, &req); + break; + case USB_REQ_SET_ADDRESS: + if (type != (USB_DIR_OUT|USB_RECIP_DEVICE)) + goto delegate; + if (le16_to_cpu(req.wLength) != 0 || + le16_to_cpu(req.wIndex) != 0) + break; + ci->address = (u8)le16_to_cpu(req.wValue); + ci->setaddr = true; + err = isr_setup_status_phase(ci); + break; + case USB_REQ_SET_FEATURE: + if (type == (USB_DIR_OUT|USB_RECIP_ENDPOINT) && + le16_to_cpu(req.wValue) == + USB_ENDPOINT_HALT) { + if (req.wLength != 0) + break; + num = le16_to_cpu(req.wIndex); + dir = (num & USB_ENDPOINT_DIR_MASK) ? TX : RX; + num &= USB_ENDPOINT_NUMBER_MASK; + if (dir == TX) + num += ci->hw_ep_max / 2; + + spin_unlock(&ci->lock); + err = _ep_set_halt(&ci->ci_hw_ep[num].ep, 1, false); + spin_lock(&ci->lock); + if (!err) + isr_setup_status_phase(ci); + } else if (type == (USB_DIR_OUT|USB_RECIP_DEVICE)) { + if (req.wLength != 0) + break; + switch (le16_to_cpu(req.wValue)) { + case USB_DEVICE_REMOTE_WAKEUP: + ci->remote_wakeup = 1; + err = isr_setup_status_phase(ci); + break; + case USB_DEVICE_TEST_MODE: + tmode = le16_to_cpu(req.wIndex) >> 8; + switch (tmode) { + case TEST_J: + case TEST_K: + case TEST_SE0_NAK: + case TEST_PACKET: + case TEST_FORCE_EN: + ci->test_mode = tmode; + err = isr_setup_status_phase( + ci); + break; + default: + break; + } + break; + case USB_DEVICE_B_HNP_ENABLE: + if (ci_otg_is_fsm_mode(ci)) { + ci->gadget.b_hnp_enable = 1; + err = isr_setup_status_phase( + ci); + } + break; + case USB_DEVICE_A_ALT_HNP_SUPPORT: + if (ci_otg_is_fsm_mode(ci)) + err = otg_a_alt_hnp_support(ci); + break; + case USB_DEVICE_A_HNP_SUPPORT: + if (ci_otg_is_fsm_mode(ci)) { + ci->gadget.a_hnp_support = 1; + err = isr_setup_status_phase( + ci); + } + break; + default: + goto delegate; + } + } else { + goto delegate; + } + break; + default: +delegate: + if (req.wLength == 0) /* no data phase */ + ci->ep0_dir = TX; + + spin_unlock(&ci->lock); + err = ci->driver->setup(&ci->gadget, &req); + spin_lock(&ci->lock); + break; + } + + if (err < 0) { + spin_unlock(&ci->lock); + if (_ep_set_halt(&hwep->ep, 1, false)) + dev_err(ci->dev, "error: _ep_set_halt\n"); + spin_lock(&ci->lock); + } +} + +/** + * isr_tr_complete_handler: transaction complete interrupt handler + * @ci: UDC descriptor + * + * This function handles traffic events + */ +static void isr_tr_complete_handler(struct ci_hdrc *ci) +__releases(ci->lock) +__acquires(ci->lock) +{ + unsigned i; + int err; + + for (i = 0; i < ci->hw_ep_max; i++) { + struct ci_hw_ep *hwep = &ci->ci_hw_ep[i]; + + if (hwep->ep.desc == NULL) + continue; /* not configured */ + + if (hw_test_and_clear_complete(ci, i)) { + err = isr_tr_complete_low(hwep); + if (hwep->type == USB_ENDPOINT_XFER_CONTROL) { + if (err > 0) /* needs status phase */ + err = isr_setup_status_phase(ci); + if (err < 0) { + spin_unlock(&ci->lock); + if (_ep_set_halt(&hwep->ep, 1, false)) + dev_err(ci->dev, + "error: _ep_set_halt\n"); + spin_lock(&ci->lock); + } + } + } + + /* Only handle setup packet below */ + if (i == 0 && + hw_test_and_clear(ci, OP_ENDPTSETUPSTAT, BIT(0))) + isr_setup_packet_handler(ci); + } +} + +/****************************************************************************** + * ENDPT block + *****************************************************************************/ +/** + * ep_enable: configure endpoint, making it usable + * + * Check usb_ep_enable() at "usb_gadget.h" for details + */ +static int ep_enable(struct usb_ep *ep, + const struct usb_endpoint_descriptor *desc) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + int retval = 0; + unsigned long flags; + u32 cap = 0; + + if (ep == NULL || desc == NULL) + return -EINVAL; + + spin_lock_irqsave(hwep->lock, flags); + + /* only internal SW should enable ctrl endpts */ + + if (!list_empty(&hwep->qh.queue)) { + dev_warn(hwep->ci->dev, "enabling a non-empty endpoint!\n"); + spin_unlock_irqrestore(hwep->lock, flags); + return -EBUSY; + } + + hwep->ep.desc = desc; + + hwep->dir = usb_endpoint_dir_in(desc) ? TX : RX; + hwep->num = usb_endpoint_num(desc); + hwep->type = usb_endpoint_type(desc); + + hwep->ep.maxpacket = usb_endpoint_maxp(desc); + hwep->ep.mult = usb_endpoint_maxp_mult(desc); + + if (hwep->type == USB_ENDPOINT_XFER_CONTROL) + cap |= QH_IOS; + + cap |= QH_ZLT; + cap |= (hwep->ep.maxpacket << __ffs(QH_MAX_PKT)) & QH_MAX_PKT; + /* + * For ISO-TX, we set mult at QH as the largest value, and use + * MultO at TD as real mult value. + */ + if (hwep->type == USB_ENDPOINT_XFER_ISOC && hwep->dir == TX) + cap |= 3 << __ffs(QH_MULT); + + hwep->qh.ptr->cap = cpu_to_le32(cap); + + hwep->qh.ptr->td.next |= cpu_to_le32(TD_TERMINATE); /* needed? */ + + if (hwep->num != 0 && hwep->type == USB_ENDPOINT_XFER_CONTROL) { + dev_err(hwep->ci->dev, "Set control xfer at non-ep0\n"); + retval = -EINVAL; + } + + /* + * Enable endpoints in the HW other than ep0 as ep0 + * is always enabled + */ + if (hwep->num) + retval |= hw_ep_enable(hwep->ci, hwep->num, hwep->dir, + hwep->type); + + spin_unlock_irqrestore(hwep->lock, flags); + return retval; +} + +/** + * ep_disable: endpoint is no longer usable + * + * Check usb_ep_disable() at "usb_gadget.h" for details + */ +static int ep_disable(struct usb_ep *ep) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + int direction, retval = 0; + unsigned long flags; + + if (ep == NULL) + return -EINVAL; + else if (hwep->ep.desc == NULL) + return -EBUSY; + + spin_lock_irqsave(hwep->lock, flags); + if (hwep->ci->gadget.speed == USB_SPEED_UNKNOWN) { + spin_unlock_irqrestore(hwep->lock, flags); + return 0; + } + + /* only internal SW should disable ctrl endpts */ + + direction = hwep->dir; + do { + retval |= _ep_nuke(hwep); + retval |= hw_ep_disable(hwep->ci, hwep->num, hwep->dir); + + if (hwep->type == USB_ENDPOINT_XFER_CONTROL) + hwep->dir = (hwep->dir == TX) ? RX : TX; + + } while (hwep->dir != direction); + + hwep->ep.desc = NULL; + + spin_unlock_irqrestore(hwep->lock, flags); + return retval; +} + +/** + * ep_alloc_request: allocate a request object to use with this endpoint + * + * Check usb_ep_alloc_request() at "usb_gadget.h" for details + */ +static struct usb_request *ep_alloc_request(struct usb_ep *ep, gfp_t gfp_flags) +{ + struct ci_hw_req *hwreq = NULL; + + if (ep == NULL) + return NULL; + + hwreq = kzalloc(sizeof(struct ci_hw_req), gfp_flags); + if (hwreq != NULL) { + INIT_LIST_HEAD(&hwreq->queue); + INIT_LIST_HEAD(&hwreq->tds); + } + + return (hwreq == NULL) ? NULL : &hwreq->req; +} + +/** + * ep_free_request: frees a request object + * + * Check usb_ep_free_request() at "usb_gadget.h" for details + */ +static void ep_free_request(struct usb_ep *ep, struct usb_request *req) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + struct ci_hw_req *hwreq = container_of(req, struct ci_hw_req, req); + struct td_node *node, *tmpnode; + unsigned long flags; + + if (ep == NULL || req == NULL) { + return; + } else if (!list_empty(&hwreq->queue)) { + dev_err(hwep->ci->dev, "freeing queued request\n"); + return; + } + + spin_lock_irqsave(hwep->lock, flags); + + list_for_each_entry_safe(node, tmpnode, &hwreq->tds, td) { + dma_pool_free(hwep->td_pool, node->ptr, node->dma); + list_del_init(&node->td); + node->ptr = NULL; + kfree(node); + } + + kfree(hwreq); + + spin_unlock_irqrestore(hwep->lock, flags); +} + +/** + * ep_queue: queues (submits) an I/O request to an endpoint + * + * Check usb_ep_queue()* at usb_gadget.h" for details + */ +static int ep_queue(struct usb_ep *ep, struct usb_request *req, + gfp_t __maybe_unused gfp_flags) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + int retval = 0; + unsigned long flags; + + if (ep == NULL || req == NULL || hwep->ep.desc == NULL) + return -EINVAL; + + spin_lock_irqsave(hwep->lock, flags); + if (hwep->ci->gadget.speed == USB_SPEED_UNKNOWN) { + spin_unlock_irqrestore(hwep->lock, flags); + return 0; + } + retval = _ep_queue(ep, req, gfp_flags); + spin_unlock_irqrestore(hwep->lock, flags); + return retval; +} + +/** + * ep_dequeue: dequeues (cancels, unlinks) an I/O request from an endpoint + * + * Check usb_ep_dequeue() at "usb_gadget.h" for details + */ +static int ep_dequeue(struct usb_ep *ep, struct usb_request *req) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + struct ci_hw_req *hwreq = container_of(req, struct ci_hw_req, req); + unsigned long flags; + struct td_node *node, *tmpnode; + + if (ep == NULL || req == NULL || hwreq->req.status != -EALREADY || + hwep->ep.desc == NULL || list_empty(&hwreq->queue) || + list_empty(&hwep->qh.queue)) + return -EINVAL; + + spin_lock_irqsave(hwep->lock, flags); + if (hwep->ci->gadget.speed != USB_SPEED_UNKNOWN) + hw_ep_flush(hwep->ci, hwep->num, hwep->dir); + + list_for_each_entry_safe(node, tmpnode, &hwreq->tds, td) { + dma_pool_free(hwep->td_pool, node->ptr, node->dma); + list_del(&node->td); + kfree(node); + } + + /* pop request */ + list_del_init(&hwreq->queue); + + usb_gadget_unmap_request(&hwep->ci->gadget, req, hwep->dir); + + req->status = -ECONNRESET; + + if (hwreq->req.complete != NULL) { + spin_unlock(hwep->lock); + usb_gadget_giveback_request(&hwep->ep, &hwreq->req); + spin_lock(hwep->lock); + } + + spin_unlock_irqrestore(hwep->lock, flags); + return 0; +} + +/** + * ep_set_halt: sets the endpoint halt feature + * + * Check usb_ep_set_halt() at "usb_gadget.h" for details + */ +static int ep_set_halt(struct usb_ep *ep, int value) +{ + return _ep_set_halt(ep, value, true); +} + +/** + * ep_set_wedge: sets the halt feature and ignores clear requests + * + * Check usb_ep_set_wedge() at "usb_gadget.h" for details + */ +static int ep_set_wedge(struct usb_ep *ep) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + unsigned long flags; + + if (ep == NULL || hwep->ep.desc == NULL) + return -EINVAL; + + spin_lock_irqsave(hwep->lock, flags); + hwep->wedge = 1; + spin_unlock_irqrestore(hwep->lock, flags); + + return usb_ep_set_halt(ep); +} + +/** + * ep_fifo_flush: flushes contents of a fifo + * + * Check usb_ep_fifo_flush() at "usb_gadget.h" for details + */ +static void ep_fifo_flush(struct usb_ep *ep) +{ + struct ci_hw_ep *hwep = container_of(ep, struct ci_hw_ep, ep); + unsigned long flags; + + if (ep == NULL) { + dev_err(hwep->ci->dev, "%02X: -EINVAL\n", _usb_addr(hwep)); + return; + } + + spin_lock_irqsave(hwep->lock, flags); + if (hwep->ci->gadget.speed == USB_SPEED_UNKNOWN) { + spin_unlock_irqrestore(hwep->lock, flags); + return; + } + + hw_ep_flush(hwep->ci, hwep->num, hwep->dir); + + spin_unlock_irqrestore(hwep->lock, flags); +} + +/** + * Endpoint-specific part of the API to the USB controller hardware + * Check "usb_gadget.h" for details + */ +static const struct usb_ep_ops usb_ep_ops = { + .enable = ep_enable, + .disable = ep_disable, + .alloc_request = ep_alloc_request, + .free_request = ep_free_request, + .queue = ep_queue, + .dequeue = ep_dequeue, + .set_halt = ep_set_halt, + .set_wedge = ep_set_wedge, + .fifo_flush = ep_fifo_flush, +}; + +/****************************************************************************** + * GADGET block + *****************************************************************************/ +static int ci_udc_vbus_session(struct usb_gadget *_gadget, int is_active) +{ + struct ci_hdrc *ci = container_of(_gadget, struct ci_hdrc, gadget); + unsigned long flags; + int gadget_ready = 0; + + spin_lock_irqsave(&ci->lock, flags); + ci->vbus_active = is_active; + if (ci->driver) + gadget_ready = 1; + spin_unlock_irqrestore(&ci->lock, flags); + + if (ci->usb_phy) + usb_phy_set_charger_state(ci->usb_phy, is_active ? + USB_CHARGER_PRESENT : USB_CHARGER_ABSENT); + + if (gadget_ready) { + if (is_active) { + pm_runtime_get_sync(&_gadget->dev); + hw_device_reset(ci); + hw_device_state(ci, ci->ep0out->qh.dma); + usb_gadget_set_state(_gadget, USB_STATE_POWERED); + usb_udc_vbus_handler(_gadget, true); + } else { + usb_udc_vbus_handler(_gadget, false); + if (ci->driver) + ci->driver->disconnect(&ci->gadget); + hw_device_state(ci, 0); + if (ci->platdata->notify_event) + ci->platdata->notify_event(ci, + CI_HDRC_CONTROLLER_STOPPED_EVENT); + _gadget_stop_activity(&ci->gadget); + pm_runtime_put_sync(&_gadget->dev); + usb_gadget_set_state(_gadget, USB_STATE_NOTATTACHED); + } + } + + return 0; +} + +static int ci_udc_wakeup(struct usb_gadget *_gadget) +{ + struct ci_hdrc *ci = container_of(_gadget, struct ci_hdrc, gadget); + unsigned long flags; + int ret = 0; + + spin_lock_irqsave(&ci->lock, flags); + if (ci->gadget.speed == USB_SPEED_UNKNOWN) { + spin_unlock_irqrestore(&ci->lock, flags); + return 0; + } + if (!ci->remote_wakeup) { + ret = -EOPNOTSUPP; + goto out; + } + if (!hw_read(ci, OP_PORTSC, PORTSC_SUSP)) { + ret = -EINVAL; + goto out; + } + hw_write(ci, OP_PORTSC, PORTSC_FPR, PORTSC_FPR); +out: + spin_unlock_irqrestore(&ci->lock, flags); + return ret; +} + +static int ci_udc_vbus_draw(struct usb_gadget *_gadget, unsigned ma) +{ + struct ci_hdrc *ci = container_of(_gadget, struct ci_hdrc, gadget); + + if (ci->usb_phy) + return usb_phy_set_power(ci->usb_phy, ma); + return -ENOTSUPP; +} + +static int ci_udc_selfpowered(struct usb_gadget *_gadget, int is_on) +{ + struct ci_hdrc *ci = container_of(_gadget, struct ci_hdrc, gadget); + struct ci_hw_ep *hwep = ci->ep0in; + unsigned long flags; + + spin_lock_irqsave(hwep->lock, flags); + _gadget->is_selfpowered = (is_on != 0); + spin_unlock_irqrestore(hwep->lock, flags); + + return 0; +} + +/* Change Data+ pullup status + * this func is used by usb_gadget_connect/disconnet + */ +static int ci_udc_pullup(struct usb_gadget *_gadget, int is_on) +{ + struct ci_hdrc *ci = container_of(_gadget, struct ci_hdrc, gadget); + + /* + * Data+ pullup controlled by OTG state machine in OTG fsm mode; + * and don't touch Data+ in host mode for dual role config. + */ + if (ci_otg_is_fsm_mode(ci) || ci->role == CI_ROLE_HOST) + return 0; + + pm_runtime_get_sync(&ci->gadget.dev); + if (is_on) + hw_write(ci, OP_USBCMD, USBCMD_RS, USBCMD_RS); + else + hw_write(ci, OP_USBCMD, USBCMD_RS, 0); + pm_runtime_put_sync(&ci->gadget.dev); + + return 0; +} + +static int ci_udc_start(struct usb_gadget *gadget, + struct usb_gadget_driver *driver); +static int ci_udc_stop(struct usb_gadget *gadget); + +/* Match ISOC IN from the highest endpoint */ +static struct usb_ep *ci_udc_match_ep(struct usb_gadget *gadget, + struct usb_endpoint_descriptor *desc, + struct usb_ss_ep_comp_descriptor *comp_desc) +{ + struct ci_hdrc *ci = container_of(gadget, struct ci_hdrc, gadget); + struct usb_ep *ep; + + if (usb_endpoint_xfer_isoc(desc) && usb_endpoint_dir_in(desc)) { + list_for_each_entry_reverse(ep, &ci->gadget.ep_list, ep_list) { + if (ep->caps.dir_in && !ep->claimed) + return ep; + } + } + + return NULL; +} + +/** + * Device operations part of the API to the USB controller hardware, + * which don't involve endpoints (or i/o) + * Check "usb_gadget.h" for details + */ +static const struct usb_gadget_ops usb_gadget_ops = { + .vbus_session = ci_udc_vbus_session, + .wakeup = ci_udc_wakeup, + .set_selfpowered = ci_udc_selfpowered, + .pullup = ci_udc_pullup, + .vbus_draw = ci_udc_vbus_draw, + .udc_start = ci_udc_start, + .udc_stop = ci_udc_stop, + .match_ep = ci_udc_match_ep, +}; + +static int init_eps(struct ci_hdrc *ci) +{ + int retval = 0, i, j; + + for (i = 0; i < ci->hw_ep_max/2; i++) + for (j = RX; j <= TX; j++) { + int k = i + j * ci->hw_ep_max/2; + struct ci_hw_ep *hwep = &ci->ci_hw_ep[k]; + + scnprintf(hwep->name, sizeof(hwep->name), "ep%i%s", i, + (j == TX) ? "in" : "out"); + + hwep->ci = ci; + hwep->lock = &ci->lock; + hwep->td_pool = ci->td_pool; + + hwep->ep.name = hwep->name; + hwep->ep.ops = &usb_ep_ops; + + if (i == 0) { + hwep->ep.caps.type_control = true; + } else { + hwep->ep.caps.type_iso = true; + hwep->ep.caps.type_bulk = true; + hwep->ep.caps.type_int = true; + } + + if (j == TX) + hwep->ep.caps.dir_in = true; + else + hwep->ep.caps.dir_out = true; + + /* + * for ep0: maxP defined in desc, for other + * eps, maxP is set by epautoconfig() called + * by gadget layer + */ + usb_ep_set_maxpacket_limit(&hwep->ep, (unsigned short)~0); + + INIT_LIST_HEAD(&hwep->qh.queue); + hwep->qh.ptr = dma_pool_zalloc(ci->qh_pool, GFP_KERNEL, + &hwep->qh.dma); + if (hwep->qh.ptr == NULL) + retval = -ENOMEM; + + /* + * set up shorthands for ep0 out and in endpoints, + * don't add to gadget's ep_list + */ + if (i == 0) { + if (j == RX) + ci->ep0out = hwep; + else + ci->ep0in = hwep; + + usb_ep_set_maxpacket_limit(&hwep->ep, CTRL_PAYLOAD_MAX); + continue; + } + + list_add_tail(&hwep->ep.ep_list, &ci->gadget.ep_list); + } + + return retval; +} + +static void destroy_eps(struct ci_hdrc *ci) +{ + int i; + + for (i = 0; i < ci->hw_ep_max; i++) { + struct ci_hw_ep *hwep = &ci->ci_hw_ep[i]; + + if (hwep->pending_td) + free_pending_td(hwep); + dma_pool_free(ci->qh_pool, hwep->qh.ptr, hwep->qh.dma); + } +} + +/** + * ci_udc_start: register a gadget driver + * @gadget: our gadget + * @driver: the driver being registered + * + * Interrupts are enabled here. + */ +static int ci_udc_start(struct usb_gadget *gadget, + struct usb_gadget_driver *driver) +{ + struct ci_hdrc *ci = container_of(gadget, struct ci_hdrc, gadget); + int retval = -ENOMEM; + + if (driver->disconnect == NULL) + return -EINVAL; + + + ci->ep0out->ep.desc = &ctrl_endpt_out_desc; + retval = usb_ep_enable(&ci->ep0out->ep); + if (retval) + return retval; + + ci->ep0in->ep.desc = &ctrl_endpt_in_desc; + retval = usb_ep_enable(&ci->ep0in->ep); + if (retval) + return retval; + + ci->driver = driver; + + /* Start otg fsm for B-device */ + if (ci_otg_is_fsm_mode(ci) && ci->fsm.id) { + ci_hdrc_otg_fsm_start(ci); + return retval; + } + + pm_runtime_get_sync(&ci->gadget.dev); + if (ci->vbus_active) { + hw_device_reset(ci); + } else { + usb_udc_vbus_handler(&ci->gadget, false); + pm_runtime_put_sync(&ci->gadget.dev); + return retval; + } + + retval = hw_device_state(ci, ci->ep0out->qh.dma); + if (retval) + pm_runtime_put_sync(&ci->gadget.dev); + + return retval; +} + +static void ci_udc_stop_for_otg_fsm(struct ci_hdrc *ci) +{ + if (!ci_otg_is_fsm_mode(ci)) + return; + + mutex_lock(&ci->fsm.lock); + if (ci->fsm.otg->state == OTG_STATE_A_PERIPHERAL) { + ci->fsm.a_bidl_adis_tmout = 1; + ci_hdrc_otg_fsm_start(ci); + } else if (ci->fsm.otg->state == OTG_STATE_B_PERIPHERAL) { + ci->fsm.protocol = PROTO_UNDEF; + ci->fsm.otg->state = OTG_STATE_UNDEFINED; + } + mutex_unlock(&ci->fsm.lock); +} + +/** + * ci_udc_stop: unregister a gadget driver + */ +static int ci_udc_stop(struct usb_gadget *gadget) +{ + struct ci_hdrc *ci = container_of(gadget, struct ci_hdrc, gadget); + unsigned long flags; + + spin_lock_irqsave(&ci->lock, flags); + + if (ci->vbus_active) { + hw_device_state(ci, 0); + spin_unlock_irqrestore(&ci->lock, flags); + if (ci->platdata->notify_event) + ci->platdata->notify_event(ci, + CI_HDRC_CONTROLLER_STOPPED_EVENT); + _gadget_stop_activity(&ci->gadget); + spin_lock_irqsave(&ci->lock, flags); + pm_runtime_put(&ci->gadget.dev); + } + + ci->driver = NULL; + spin_unlock_irqrestore(&ci->lock, flags); + + ci_udc_stop_for_otg_fsm(ci); + return 0; +} + +/****************************************************************************** + * BUS block + *****************************************************************************/ +/** + * udc_irq: ci interrupt handler + * + * This function returns IRQ_HANDLED if the IRQ has been handled + * It locks access to registers + */ +static irqreturn_t udc_irq(struct ci_hdrc *ci) +{ + irqreturn_t retval; + u32 intr; + + if (ci == NULL) + return IRQ_HANDLED; + + spin_lock(&ci->lock); + + if (ci->platdata->flags & CI_HDRC_REGS_SHARED) { + if (hw_read(ci, OP_USBMODE, USBMODE_CM) != + USBMODE_CM_DC) { + spin_unlock(&ci->lock); + return IRQ_NONE; + } + } + intr = hw_test_and_clear_intr_active(ci); + + if (intr) { + /* order defines priority - do NOT change it */ + if (USBi_URI & intr) + isr_reset_handler(ci); + + if (USBi_PCI & intr) { + ci->gadget.speed = hw_port_is_high_speed(ci) ? + USB_SPEED_HIGH : USB_SPEED_FULL; + if (ci->suspended) { + if (ci->driver->resume) { + spin_unlock(&ci->lock); + ci->driver->resume(&ci->gadget); + spin_lock(&ci->lock); + } + ci->suspended = 0; + usb_gadget_set_state(&ci->gadget, + ci->resume_state); + } + } + + if (USBi_UI & intr) + isr_tr_complete_handler(ci); + + if ((USBi_SLI & intr) && !(ci->suspended)) { + ci->suspended = 1; + ci->resume_state = ci->gadget.state; + if (ci->gadget.speed != USB_SPEED_UNKNOWN && + ci->driver->suspend) { + spin_unlock(&ci->lock); + ci->driver->suspend(&ci->gadget); + spin_lock(&ci->lock); + } + usb_gadget_set_state(&ci->gadget, + USB_STATE_SUSPENDED); + } + retval = IRQ_HANDLED; + } else { + retval = IRQ_NONE; + } + spin_unlock(&ci->lock); + + return retval; +} + +/** + * udc_start: initialize gadget role + * @ci: chipidea controller + */ +static int udc_start(struct ci_hdrc *ci) +{ + struct device *dev = ci->dev; + struct usb_otg_caps *otg_caps = &ci->platdata->ci_otg_caps; + int retval = 0; + + ci->gadget.ops = &usb_gadget_ops; + ci->gadget.speed = USB_SPEED_UNKNOWN; + ci->gadget.max_speed = USB_SPEED_HIGH; + ci->gadget.name = ci->platdata->name; + ci->gadget.otg_caps = otg_caps; + + if (ci->platdata->flags & CI_HDRC_REQUIRES_ALIGNED_DMA) + ci->gadget.quirk_avoids_skb_reserve = 1; + + if (ci->is_otg && (otg_caps->hnp_support || otg_caps->srp_support || + otg_caps->adp_support)) + ci->gadget.is_otg = 1; + + INIT_LIST_HEAD(&ci->gadget.ep_list); + + /* alloc resources */ + ci->qh_pool = dma_pool_create("ci_hw_qh", dev->parent, + sizeof(struct ci_hw_qh), + 64, CI_HDRC_PAGE_SIZE); + if (ci->qh_pool == NULL) + return -ENOMEM; + + ci->td_pool = dma_pool_create("ci_hw_td", dev->parent, + sizeof(struct ci_hw_td), + 64, CI_HDRC_PAGE_SIZE); + if (ci->td_pool == NULL) { + retval = -ENOMEM; + goto free_qh_pool; + } + + retval = init_eps(ci); + if (retval) + goto free_pools; + + ci->gadget.ep0 = &ci->ep0in->ep; + + retval = usb_add_gadget_udc(dev, &ci->gadget); + if (retval) + goto destroy_eps; + + pm_runtime_no_callbacks(&ci->gadget.dev); + pm_runtime_enable(&ci->gadget.dev); + + return retval; + +destroy_eps: + destroy_eps(ci); +free_pools: + dma_pool_destroy(ci->td_pool); +free_qh_pool: + dma_pool_destroy(ci->qh_pool); + return retval; +} + +/** + * ci_hdrc_gadget_destroy: parent remove must call this to remove UDC + * + * No interrupts active, the IRQ has been released + */ +void ci_hdrc_gadget_destroy(struct ci_hdrc *ci) +{ + if (!ci->roles[CI_ROLE_GADGET]) + return; + + usb_del_gadget_udc(&ci->gadget); + + destroy_eps(ci); + + dma_pool_destroy(ci->td_pool); + dma_pool_destroy(ci->qh_pool); +} + +static int udc_id_switch_for_device(struct ci_hdrc *ci) +{ + if (ci->is_otg) + /* Clear and enable BSV irq */ + hw_write_otgsc(ci, OTGSC_BSVIS | OTGSC_BSVIE, + OTGSC_BSVIS | OTGSC_BSVIE); + + return 0; +} + +static void udc_id_switch_for_host(struct ci_hdrc *ci) +{ + /* + * host doesn't care B_SESSION_VALID event + * so clear and disbale BSV irq + */ + if (ci->is_otg) + hw_write_otgsc(ci, OTGSC_BSVIE | OTGSC_BSVIS, OTGSC_BSVIS); + + ci->vbus_active = 0; +} + +/** + * ci_hdrc_gadget_init - initialize device related bits + * ci: the controller + * + * This function initializes the gadget, if the device is "device capable". + */ +int ci_hdrc_gadget_init(struct ci_hdrc *ci) +{ + struct ci_role_driver *rdrv; + int ret; + + if (!hw_read(ci, CAP_DCCPARAMS, DCCPARAMS_DC)) + return -ENXIO; + + rdrv = devm_kzalloc(ci->dev, sizeof(*rdrv), GFP_KERNEL); + if (!rdrv) + return -ENOMEM; + + rdrv->start = udc_id_switch_for_device; + rdrv->stop = udc_id_switch_for_host; + rdrv->irq = udc_irq; + rdrv->name = "gadget"; + + ret = udc_start(ci); + if (!ret) + ci->roles[CI_ROLE_GADGET] = rdrv; + + return ret; +} diff --git a/drivers/usb/chipidea/udc.h b/drivers/usb/chipidea/udc.h new file mode 100644 index 000000000..e023735d9 --- /dev/null +++ b/drivers/usb/chipidea/udc.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * udc.h - ChipIdea UDC structures + * + * Copyright (C) 2008 Chipidea - MIPS Technologies, Inc. All rights reserved. + * + * Author: David Lopo + */ + +#ifndef __DRIVERS_USB_CHIPIDEA_UDC_H +#define __DRIVERS_USB_CHIPIDEA_UDC_H + +#include <linux/list.h> + +#define CTRL_PAYLOAD_MAX 64 +#define RX 0 /* similar to USB_DIR_OUT but can be used as an index */ +#define TX 1 /* similar to USB_DIR_IN but can be used as an index */ + +/* DMA layout of transfer descriptors */ +struct ci_hw_td { + /* 0 */ + __le32 next; +#define TD_TERMINATE BIT(0) +#define TD_ADDR_MASK (0xFFFFFFEUL << 5) + /* 1 */ + __le32 token; +#define TD_STATUS (0x00FFUL << 0) +#define TD_STATUS_TR_ERR BIT(3) +#define TD_STATUS_DT_ERR BIT(5) +#define TD_STATUS_HALTED BIT(6) +#define TD_STATUS_ACTIVE BIT(7) +#define TD_MULTO (0x0003UL << 10) +#define TD_IOC BIT(15) +#define TD_TOTAL_BYTES (0x7FFFUL << 16) + /* 2 */ + __le32 page[5]; +#define TD_CURR_OFFSET (0x0FFFUL << 0) +#define TD_FRAME_NUM (0x07FFUL << 0) +#define TD_RESERVED_MASK (0x0FFFUL << 0) +} __attribute__ ((packed, aligned(4))); + +/* DMA layout of queue heads */ +struct ci_hw_qh { + /* 0 */ + __le32 cap; +#define QH_IOS BIT(15) +#define QH_MAX_PKT (0x07FFUL << 16) +#define QH_ZLT BIT(29) +#define QH_MULT (0x0003UL << 30) +#define QH_ISO_MULT(x) ((x >> 11) & 0x03) + /* 1 */ + __le32 curr; + /* 2 - 8 */ + struct ci_hw_td td; + /* 9 */ + __le32 RESERVED; + struct usb_ctrlrequest setup; +} __attribute__ ((packed, aligned(4))); + +struct td_node { + struct list_head td; + dma_addr_t dma; + struct ci_hw_td *ptr; +}; + +/** + * struct ci_hw_req - usb request representation + * @req: request structure for gadget drivers + * @queue: link to QH list + * @ptr: transfer descriptor for this request + * @dma: dma address for the transfer descriptor + * @zptr: transfer descriptor for the zero packet + * @zdma: dma address of the zero packet's transfer descriptor + */ +struct ci_hw_req { + struct usb_request req; + struct list_head queue; + struct list_head tds; +}; + +#ifdef CONFIG_USB_CHIPIDEA_UDC + +int ci_hdrc_gadget_init(struct ci_hdrc *ci); +void ci_hdrc_gadget_destroy(struct ci_hdrc *ci); + +#else + +static inline int ci_hdrc_gadget_init(struct ci_hdrc *ci) +{ + return -ENXIO; +} + +static inline void ci_hdrc_gadget_destroy(struct ci_hdrc *ci) +{ + +} + +#endif + +#endif /* __DRIVERS_USB_CHIPIDEA_UDC_H */ diff --git a/drivers/usb/chipidea/ulpi.c b/drivers/usb/chipidea/ulpi.c new file mode 100644 index 000000000..dfec07e8a --- /dev/null +++ b/drivers/usb/chipidea/ulpi.c @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2016 Linaro Ltd. + */ + +#include <linux/device.h> +#include <linux/usb/chipidea.h> +#include <linux/ulpi/interface.h> + +#include "ci.h" + +#define ULPI_WAKEUP BIT(31) +#define ULPI_RUN BIT(30) +#define ULPI_WRITE BIT(29) +#define ULPI_SYNC_STATE BIT(27) +#define ULPI_ADDR(n) ((n) << 16) +#define ULPI_DATA(n) (n) + +static int ci_ulpi_wait(struct ci_hdrc *ci, u32 mask) +{ + unsigned long usec = 10000; + + while (usec--) { + if (!hw_read(ci, OP_ULPI_VIEWPORT, mask)) + return 0; + + udelay(1); + } + + return -ETIMEDOUT; +} + +static int ci_ulpi_read(struct device *dev, u8 addr) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + int ret; + + hw_write(ci, OP_ULPI_VIEWPORT, 0xffffffff, ULPI_WRITE | ULPI_WAKEUP); + ret = ci_ulpi_wait(ci, ULPI_WAKEUP); + if (ret) + return ret; + + hw_write(ci, OP_ULPI_VIEWPORT, 0xffffffff, ULPI_RUN | ULPI_ADDR(addr)); + ret = ci_ulpi_wait(ci, ULPI_RUN); + if (ret) + return ret; + + return hw_read(ci, OP_ULPI_VIEWPORT, GENMASK(15, 8)) >> 8; +} + +static int ci_ulpi_write(struct device *dev, u8 addr, u8 val) +{ + struct ci_hdrc *ci = dev_get_drvdata(dev); + int ret; + + hw_write(ci, OP_ULPI_VIEWPORT, 0xffffffff, ULPI_WRITE | ULPI_WAKEUP); + ret = ci_ulpi_wait(ci, ULPI_WAKEUP); + if (ret) + return ret; + + hw_write(ci, OP_ULPI_VIEWPORT, 0xffffffff, + ULPI_RUN | ULPI_WRITE | ULPI_ADDR(addr) | val); + return ci_ulpi_wait(ci, ULPI_RUN); +} + +int ci_ulpi_init(struct ci_hdrc *ci) +{ + if (ci->platdata->phy_mode != USBPHY_INTERFACE_MODE_ULPI) + return 0; + + /* + * Set PORTSC correctly so we can read/write ULPI registers for + * identification purposes + */ + hw_phymode_configure(ci); + + ci->ulpi_ops.read = ci_ulpi_read; + ci->ulpi_ops.write = ci_ulpi_write; + ci->ulpi = ulpi_register_interface(ci->dev, &ci->ulpi_ops); + if (IS_ERR(ci->ulpi)) + dev_err(ci->dev, "failed to register ULPI interface"); + + return PTR_ERR_OR_ZERO(ci->ulpi); +} + +void ci_ulpi_exit(struct ci_hdrc *ci) +{ + if (ci->ulpi) { + ulpi_unregister_interface(ci->ulpi); + ci->ulpi = NULL; + } +} + +int ci_ulpi_resume(struct ci_hdrc *ci) +{ + int cnt = 100000; + + if (ci->platdata->phy_mode != USBPHY_INTERFACE_MODE_ULPI) + return 0; + + while (cnt-- > 0) { + if (hw_read(ci, OP_ULPI_VIEWPORT, ULPI_SYNC_STATE)) + return 0; + udelay(1); + } + + return -ETIMEDOUT; +} diff --git a/drivers/usb/chipidea/usbmisc_imx.c b/drivers/usb/chipidea/usbmisc_imx.c new file mode 100644 index 000000000..424ecb1f0 --- /dev/null +++ b/drivers/usb/chipidea/usbmisc_imx.c @@ -0,0 +1,640 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2012 Freescale Semiconductor, Inc. + */ + +#include <linux/module.h> +#include <linux/of_platform.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/delay.h> + +#include "ci_hdrc_imx.h" + +#define MX25_USB_PHY_CTRL_OFFSET 0x08 +#define MX25_BM_EXTERNAL_VBUS_DIVIDER BIT(23) + +#define MX25_EHCI_INTERFACE_SINGLE_UNI (2 << 0) +#define MX25_EHCI_INTERFACE_DIFF_UNI (0 << 0) +#define MX25_EHCI_INTERFACE_MASK (0xf) + +#define MX25_OTG_SIC_SHIFT 29 +#define MX25_OTG_SIC_MASK (0x3 << MX25_OTG_SIC_SHIFT) +#define MX25_OTG_PM_BIT BIT(24) +#define MX25_OTG_PP_BIT BIT(11) +#define MX25_OTG_OCPOL_BIT BIT(3) + +#define MX25_H1_SIC_SHIFT 21 +#define MX25_H1_SIC_MASK (0x3 << MX25_H1_SIC_SHIFT) +#define MX25_H1_PP_BIT BIT(18) +#define MX25_H1_PM_BIT BIT(16) +#define MX25_H1_IPPUE_UP_BIT BIT(7) +#define MX25_H1_IPPUE_DOWN_BIT BIT(6) +#define MX25_H1_TLL_BIT BIT(5) +#define MX25_H1_USBTE_BIT BIT(4) +#define MX25_H1_OCPOL_BIT BIT(2) + +#define MX27_H1_PM_BIT BIT(8) +#define MX27_H2_PM_BIT BIT(16) +#define MX27_OTG_PM_BIT BIT(24) + +#define MX53_USB_OTG_PHY_CTRL_0_OFFSET 0x08 +#define MX53_USB_OTG_PHY_CTRL_1_OFFSET 0x0c +#define MX53_USB_CTRL_1_OFFSET 0x10 +#define MX53_USB_CTRL_1_H2_XCVR_CLK_SEL_MASK (0x11 << 2) +#define MX53_USB_CTRL_1_H2_XCVR_CLK_SEL_ULPI BIT(2) +#define MX53_USB_CTRL_1_H3_XCVR_CLK_SEL_MASK (0x11 << 6) +#define MX53_USB_CTRL_1_H3_XCVR_CLK_SEL_ULPI BIT(6) +#define MX53_USB_UH2_CTRL_OFFSET 0x14 +#define MX53_USB_UH3_CTRL_OFFSET 0x18 +#define MX53_USB_CLKONOFF_CTRL_OFFSET 0x24 +#define MX53_USB_CLKONOFF_CTRL_H2_INT60CKOFF BIT(21) +#define MX53_USB_CLKONOFF_CTRL_H3_INT60CKOFF BIT(22) +#define MX53_BM_OVER_CUR_DIS_H1 BIT(5) +#define MX53_BM_OVER_CUR_DIS_OTG BIT(8) +#define MX53_BM_OVER_CUR_DIS_UHx BIT(30) +#define MX53_USB_CTRL_1_UH2_ULPI_EN BIT(26) +#define MX53_USB_CTRL_1_UH3_ULPI_EN BIT(27) +#define MX53_USB_UHx_CTRL_WAKE_UP_EN BIT(7) +#define MX53_USB_UHx_CTRL_ULPI_INT_EN BIT(8) +#define MX53_USB_PHYCTRL1_PLLDIV_MASK 0x3 +#define MX53_USB_PLL_DIV_24_MHZ 0x01 + +#define MX6_BM_NON_BURST_SETTING BIT(1) +#define MX6_BM_OVER_CUR_DIS BIT(7) +#define MX6_BM_OVER_CUR_POLARITY BIT(8) +#define MX6_BM_WAKEUP_ENABLE BIT(10) +#define MX6_BM_ID_WAKEUP BIT(16) +#define MX6_BM_VBUS_WAKEUP BIT(17) +#define MX6SX_BM_DPDM_WAKEUP_EN BIT(29) +#define MX6_BM_WAKEUP_INTR BIT(31) +#define MX6_USB_OTG1_PHY_CTRL 0x18 +/* For imx6dql, it is host-only controller, for later imx6, it is otg's */ +#define MX6_USB_OTG2_PHY_CTRL 0x1c +#define MX6SX_USB_VBUS_WAKEUP_SOURCE(v) (v << 8) +#define MX6SX_USB_VBUS_WAKEUP_SOURCE_VBUS MX6SX_USB_VBUS_WAKEUP_SOURCE(0) +#define MX6SX_USB_VBUS_WAKEUP_SOURCE_AVALID MX6SX_USB_VBUS_WAKEUP_SOURCE(1) +#define MX6SX_USB_VBUS_WAKEUP_SOURCE_BVALID MX6SX_USB_VBUS_WAKEUP_SOURCE(2) +#define MX6SX_USB_VBUS_WAKEUP_SOURCE_SESS_END MX6SX_USB_VBUS_WAKEUP_SOURCE(3) + +#define VF610_OVER_CUR_DIS BIT(7) + +#define MX7D_USBNC_USB_CTRL2 0x4 +#define MX7D_USB_VBUS_WAKEUP_SOURCE_MASK 0x3 +#define MX7D_USB_VBUS_WAKEUP_SOURCE(v) (v << 0) +#define MX7D_USB_VBUS_WAKEUP_SOURCE_VBUS MX7D_USB_VBUS_WAKEUP_SOURCE(0) +#define MX7D_USB_VBUS_WAKEUP_SOURCE_AVALID MX7D_USB_VBUS_WAKEUP_SOURCE(1) +#define MX7D_USB_VBUS_WAKEUP_SOURCE_BVALID MX7D_USB_VBUS_WAKEUP_SOURCE(2) +#define MX7D_USB_VBUS_WAKEUP_SOURCE_SESS_END MX7D_USB_VBUS_WAKEUP_SOURCE(3) + +struct usbmisc_ops { + /* It's called once when probe a usb device */ + int (*init)(struct imx_usbmisc_data *data); + /* It's called once after adding a usb device */ + int (*post)(struct imx_usbmisc_data *data); + /* It's called when we need to enable/disable usb wakeup */ + int (*set_wakeup)(struct imx_usbmisc_data *data, bool enabled); +}; + +struct imx_usbmisc { + void __iomem *base; + spinlock_t lock; + const struct usbmisc_ops *ops; +}; + +static inline bool is_imx53_usbmisc(struct imx_usbmisc_data *data); + +static int usbmisc_imx25_init(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + unsigned long flags; + u32 val = 0; + + if (data->index > 1) + return -EINVAL; + + spin_lock_irqsave(&usbmisc->lock, flags); + switch (data->index) { + case 0: + val = readl(usbmisc->base); + val &= ~(MX25_OTG_SIC_MASK | MX25_OTG_PP_BIT); + val |= (MX25_EHCI_INTERFACE_DIFF_UNI & MX25_EHCI_INTERFACE_MASK) << MX25_OTG_SIC_SHIFT; + val |= (MX25_OTG_PM_BIT | MX25_OTG_OCPOL_BIT); + writel(val, usbmisc->base); + break; + case 1: + val = readl(usbmisc->base); + val &= ~(MX25_H1_SIC_MASK | MX25_H1_PP_BIT | MX25_H1_IPPUE_UP_BIT); + val |= (MX25_EHCI_INTERFACE_SINGLE_UNI & MX25_EHCI_INTERFACE_MASK) << MX25_H1_SIC_SHIFT; + val |= (MX25_H1_PM_BIT | MX25_H1_OCPOL_BIT | MX25_H1_TLL_BIT | + MX25_H1_USBTE_BIT | MX25_H1_IPPUE_DOWN_BIT); + + writel(val, usbmisc->base); + + break; + } + spin_unlock_irqrestore(&usbmisc->lock, flags); + + return 0; +} + +static int usbmisc_imx25_post(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + void __iomem *reg; + unsigned long flags; + u32 val; + + if (data->index > 2) + return -EINVAL; + + if (data->index) + return 0; + + spin_lock_irqsave(&usbmisc->lock, flags); + reg = usbmisc->base + MX25_USB_PHY_CTRL_OFFSET; + val = readl(reg); + + if (data->evdo) + val |= MX25_BM_EXTERNAL_VBUS_DIVIDER; + else + val &= ~MX25_BM_EXTERNAL_VBUS_DIVIDER; + + writel(val, reg); + spin_unlock_irqrestore(&usbmisc->lock, flags); + usleep_range(5000, 10000); /* needed to stabilize voltage */ + + return 0; +} + +static int usbmisc_imx27_init(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + unsigned long flags; + u32 val; + + switch (data->index) { + case 0: + val = MX27_OTG_PM_BIT; + break; + case 1: + val = MX27_H1_PM_BIT; + break; + case 2: + val = MX27_H2_PM_BIT; + break; + default: + return -EINVAL; + } + + spin_lock_irqsave(&usbmisc->lock, flags); + if (data->disable_oc) + val = readl(usbmisc->base) | val; + else + val = readl(usbmisc->base) & ~val; + writel(val, usbmisc->base); + spin_unlock_irqrestore(&usbmisc->lock, flags); + + return 0; +} + +static int usbmisc_imx53_init(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + void __iomem *reg = NULL; + unsigned long flags; + u32 val = 0; + + if (data->index > 3) + return -EINVAL; + + /* Select a 24 MHz reference clock for the PHY */ + val = readl(usbmisc->base + MX53_USB_OTG_PHY_CTRL_1_OFFSET); + val &= ~MX53_USB_PHYCTRL1_PLLDIV_MASK; + val |= MX53_USB_PLL_DIV_24_MHZ; + writel(val, usbmisc->base + MX53_USB_OTG_PHY_CTRL_1_OFFSET); + + spin_lock_irqsave(&usbmisc->lock, flags); + + switch (data->index) { + case 0: + if (data->disable_oc) { + reg = usbmisc->base + MX53_USB_OTG_PHY_CTRL_0_OFFSET; + val = readl(reg) | MX53_BM_OVER_CUR_DIS_OTG; + writel(val, reg); + } + break; + case 1: + if (data->disable_oc) { + reg = usbmisc->base + MX53_USB_OTG_PHY_CTRL_0_OFFSET; + val = readl(reg) | MX53_BM_OVER_CUR_DIS_H1; + writel(val, reg); + } + break; + case 2: + if (data->ulpi) { + /* set USBH2 into ULPI-mode. */ + reg = usbmisc->base + MX53_USB_CTRL_1_OFFSET; + val = readl(reg) | MX53_USB_CTRL_1_UH2_ULPI_EN; + /* select ULPI clock */ + val &= ~MX53_USB_CTRL_1_H2_XCVR_CLK_SEL_MASK; + val |= MX53_USB_CTRL_1_H2_XCVR_CLK_SEL_ULPI; + writel(val, reg); + /* Set interrupt wake up enable */ + reg = usbmisc->base + MX53_USB_UH2_CTRL_OFFSET; + val = readl(reg) | MX53_USB_UHx_CTRL_WAKE_UP_EN + | MX53_USB_UHx_CTRL_ULPI_INT_EN; + writel(val, reg); + if (is_imx53_usbmisc(data)) { + /* Disable internal 60Mhz clock */ + reg = usbmisc->base + + MX53_USB_CLKONOFF_CTRL_OFFSET; + val = readl(reg) | + MX53_USB_CLKONOFF_CTRL_H2_INT60CKOFF; + writel(val, reg); + } + + } + if (data->disable_oc) { + reg = usbmisc->base + MX53_USB_UH2_CTRL_OFFSET; + val = readl(reg) | MX53_BM_OVER_CUR_DIS_UHx; + writel(val, reg); + } + break; + case 3: + if (data->ulpi) { + /* set USBH3 into ULPI-mode. */ + reg = usbmisc->base + MX53_USB_CTRL_1_OFFSET; + val = readl(reg) | MX53_USB_CTRL_1_UH3_ULPI_EN; + /* select ULPI clock */ + val &= ~MX53_USB_CTRL_1_H3_XCVR_CLK_SEL_MASK; + val |= MX53_USB_CTRL_1_H3_XCVR_CLK_SEL_ULPI; + writel(val, reg); + /* Set interrupt wake up enable */ + reg = usbmisc->base + MX53_USB_UH3_CTRL_OFFSET; + val = readl(reg) | MX53_USB_UHx_CTRL_WAKE_UP_EN + | MX53_USB_UHx_CTRL_ULPI_INT_EN; + writel(val, reg); + + if (is_imx53_usbmisc(data)) { + /* Disable internal 60Mhz clock */ + reg = usbmisc->base + + MX53_USB_CLKONOFF_CTRL_OFFSET; + val = readl(reg) | + MX53_USB_CLKONOFF_CTRL_H3_INT60CKOFF; + writel(val, reg); + } + } + if (data->disable_oc) { + reg = usbmisc->base + MX53_USB_UH3_CTRL_OFFSET; + val = readl(reg) | MX53_BM_OVER_CUR_DIS_UHx; + writel(val, reg); + } + break; + } + + spin_unlock_irqrestore(&usbmisc->lock, flags); + + return 0; +} + +static int usbmisc_imx6q_set_wakeup + (struct imx_usbmisc_data *data, bool enabled) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + unsigned long flags; + u32 val; + u32 wakeup_setting = (MX6_BM_WAKEUP_ENABLE | + MX6_BM_VBUS_WAKEUP | MX6_BM_ID_WAKEUP); + int ret = 0; + + if (data->index > 3) + return -EINVAL; + + spin_lock_irqsave(&usbmisc->lock, flags); + val = readl(usbmisc->base + data->index * 4); + if (enabled) { + val |= wakeup_setting; + } else { + if (val & MX6_BM_WAKEUP_INTR) + pr_debug("wakeup int at ci_hdrc.%d\n", data->index); + val &= ~wakeup_setting; + } + writel(val, usbmisc->base + data->index * 4); + spin_unlock_irqrestore(&usbmisc->lock, flags); + + return ret; +} + +static int usbmisc_imx6q_init(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + unsigned long flags; + u32 reg; + + if (data->index > 3) + return -EINVAL; + + spin_lock_irqsave(&usbmisc->lock, flags); + + reg = readl(usbmisc->base + data->index * 4); + if (data->disable_oc) { + reg |= MX6_BM_OVER_CUR_DIS; + } else if (data->oc_polarity == 1) { + /* High active */ + reg &= ~(MX6_BM_OVER_CUR_DIS | MX6_BM_OVER_CUR_POLARITY); + } else { + reg &= ~(MX6_BM_OVER_CUR_DIS); + } + writel(reg, usbmisc->base + data->index * 4); + + /* SoC non-burst setting */ + reg = readl(usbmisc->base + data->index * 4); + writel(reg | MX6_BM_NON_BURST_SETTING, + usbmisc->base + data->index * 4); + + spin_unlock_irqrestore(&usbmisc->lock, flags); + + usbmisc_imx6q_set_wakeup(data, false); + + return 0; +} + +static int usbmisc_imx6sx_init(struct imx_usbmisc_data *data) +{ + void __iomem *reg = NULL; + unsigned long flags; + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + u32 val; + + usbmisc_imx6q_init(data); + + if (data->index == 0 || data->index == 1) { + reg = usbmisc->base + MX6_USB_OTG1_PHY_CTRL + data->index * 4; + spin_lock_irqsave(&usbmisc->lock, flags); + /* Set vbus wakeup source as bvalid */ + val = readl(reg); + writel(val | MX6SX_USB_VBUS_WAKEUP_SOURCE_BVALID, reg); + /* + * Disable dp/dm wakeup in device mode when vbus is + * not there. + */ + val = readl(usbmisc->base + data->index * 4); + writel(val & ~MX6SX_BM_DPDM_WAKEUP_EN, + usbmisc->base + data->index * 4); + spin_unlock_irqrestore(&usbmisc->lock, flags); + } + + return 0; +} + +static int usbmisc_vf610_init(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + u32 reg; + + /* + * Vybrid only has one misc register set, but in two different + * areas. These is reflected in two instances of this driver. + */ + if (data->index >= 1) + return -EINVAL; + + if (data->disable_oc) { + reg = readl(usbmisc->base); + writel(reg | VF610_OVER_CUR_DIS, usbmisc->base); + } + + return 0; +} + +static int usbmisc_imx7d_set_wakeup + (struct imx_usbmisc_data *data, bool enabled) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + unsigned long flags; + u32 val; + u32 wakeup_setting = (MX6_BM_WAKEUP_ENABLE | + MX6_BM_VBUS_WAKEUP | MX6_BM_ID_WAKEUP); + + spin_lock_irqsave(&usbmisc->lock, flags); + val = readl(usbmisc->base); + if (enabled) { + writel(val | wakeup_setting, usbmisc->base); + } else { + if (val & MX6_BM_WAKEUP_INTR) + dev_dbg(data->dev, "wakeup int\n"); + writel(val & ~wakeup_setting, usbmisc->base); + } + spin_unlock_irqrestore(&usbmisc->lock, flags); + + return 0; +} + +static int usbmisc_imx7d_init(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + unsigned long flags; + u32 reg; + + if (data->index >= 1) + return -EINVAL; + + spin_lock_irqsave(&usbmisc->lock, flags); + reg = readl(usbmisc->base); + if (data->disable_oc) { + reg |= MX6_BM_OVER_CUR_DIS; + } else if (data->oc_polarity == 1) { + /* High active */ + reg &= ~(MX6_BM_OVER_CUR_DIS | MX6_BM_OVER_CUR_POLARITY); + } + writel(reg, usbmisc->base); + + reg = readl(usbmisc->base + MX7D_USBNC_USB_CTRL2); + reg &= ~MX7D_USB_VBUS_WAKEUP_SOURCE_MASK; + writel(reg | MX7D_USB_VBUS_WAKEUP_SOURCE_BVALID, + usbmisc->base + MX7D_USBNC_USB_CTRL2); + spin_unlock_irqrestore(&usbmisc->lock, flags); + + usbmisc_imx7d_set_wakeup(data, false); + + return 0; +} + +static const struct usbmisc_ops imx25_usbmisc_ops = { + .init = usbmisc_imx25_init, + .post = usbmisc_imx25_post, +}; + +static const struct usbmisc_ops imx27_usbmisc_ops = { + .init = usbmisc_imx27_init, +}; + +static const struct usbmisc_ops imx51_usbmisc_ops = { + .init = usbmisc_imx53_init, +}; + +static const struct usbmisc_ops imx53_usbmisc_ops = { + .init = usbmisc_imx53_init, +}; + +static const struct usbmisc_ops imx6q_usbmisc_ops = { + .set_wakeup = usbmisc_imx6q_set_wakeup, + .init = usbmisc_imx6q_init, +}; + +static const struct usbmisc_ops vf610_usbmisc_ops = { + .init = usbmisc_vf610_init, +}; + +static const struct usbmisc_ops imx6sx_usbmisc_ops = { + .set_wakeup = usbmisc_imx6q_set_wakeup, + .init = usbmisc_imx6sx_init, +}; + +static const struct usbmisc_ops imx7d_usbmisc_ops = { + .init = usbmisc_imx7d_init, + .set_wakeup = usbmisc_imx7d_set_wakeup, +}; + +static inline bool is_imx53_usbmisc(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc = dev_get_drvdata(data->dev); + + return usbmisc->ops == &imx53_usbmisc_ops; +} + +int imx_usbmisc_init(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc; + + if (!data) + return 0; + + usbmisc = dev_get_drvdata(data->dev); + if (!usbmisc->ops->init) + return 0; + return usbmisc->ops->init(data); +} +EXPORT_SYMBOL_GPL(imx_usbmisc_init); + +int imx_usbmisc_init_post(struct imx_usbmisc_data *data) +{ + struct imx_usbmisc *usbmisc; + + if (!data) + return 0; + + usbmisc = dev_get_drvdata(data->dev); + if (!usbmisc->ops->post) + return 0; + return usbmisc->ops->post(data); +} +EXPORT_SYMBOL_GPL(imx_usbmisc_init_post); + +int imx_usbmisc_set_wakeup(struct imx_usbmisc_data *data, bool enabled) +{ + struct imx_usbmisc *usbmisc; + + if (!data) + return 0; + + usbmisc = dev_get_drvdata(data->dev); + if (!usbmisc->ops->set_wakeup) + return 0; + return usbmisc->ops->set_wakeup(data, enabled); +} +EXPORT_SYMBOL_GPL(imx_usbmisc_set_wakeup); + +static const struct of_device_id usbmisc_imx_dt_ids[] = { + { + .compatible = "fsl,imx25-usbmisc", + .data = &imx25_usbmisc_ops, + }, + { + .compatible = "fsl,imx35-usbmisc", + .data = &imx25_usbmisc_ops, + }, + { + .compatible = "fsl,imx27-usbmisc", + .data = &imx27_usbmisc_ops, + }, + { + .compatible = "fsl,imx51-usbmisc", + .data = &imx51_usbmisc_ops, + }, + { + .compatible = "fsl,imx53-usbmisc", + .data = &imx53_usbmisc_ops, + }, + { + .compatible = "fsl,imx6q-usbmisc", + .data = &imx6q_usbmisc_ops, + }, + { + .compatible = "fsl,vf610-usbmisc", + .data = &vf610_usbmisc_ops, + }, + { + .compatible = "fsl,imx6sx-usbmisc", + .data = &imx6sx_usbmisc_ops, + }, + { + .compatible = "fsl,imx6ul-usbmisc", + .data = &imx6sx_usbmisc_ops, + }, + { + .compatible = "fsl,imx7d-usbmisc", + .data = &imx7d_usbmisc_ops, + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, usbmisc_imx_dt_ids); + +static int usbmisc_imx_probe(struct platform_device *pdev) +{ + struct resource *res; + struct imx_usbmisc *data; + const struct of_device_id *of_id; + + of_id = of_match_device(usbmisc_imx_dt_ids, &pdev->dev); + if (!of_id) + return -ENODEV; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + spin_lock_init(&data->lock); + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + data->base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(data->base)) + return PTR_ERR(data->base); + + data->ops = (const struct usbmisc_ops *)of_id->data; + platform_set_drvdata(pdev, data); + + return 0; +} + +static int usbmisc_imx_remove(struct platform_device *pdev) +{ + return 0; +} + +static struct platform_driver usbmisc_imx_driver = { + .probe = usbmisc_imx_probe, + .remove = usbmisc_imx_remove, + .driver = { + .name = "usbmisc_imx", + .of_match_table = usbmisc_imx_dt_ids, + }, +}; + +module_platform_driver(usbmisc_imx_driver); + +MODULE_ALIAS("platform:usbmisc-imx"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("driver for imx usb non-core registers"); +MODULE_AUTHOR("Richard Zhao <richard.zhao@freescale.com>"); |