diff options
author | Peter Robinson <pbrobinson@gmail.com> | 2016-07-19 09:00:32 +0100 |
---|---|---|
committer | Peter Robinson <pbrobinson@gmail.com> | 2016-07-19 09:00:32 +0100 |
commit | a84c0224bb233a4942301a5075f00dcca596f334 (patch) | |
tree | 87ec40ecb74c03f4fce6a5ba209fec0d14438f74 /arm64-pcie-quirks-xgene.patch | |
parent | 7a27e68a3112162e825513c4a9e09e45de8fe1bc (diff) | |
download | kernel-a84c0224bb233a4942301a5075f00dcca596f334.tar.gz kernel-a84c0224bb233a4942301a5075f00dcca596f334.tar.xz kernel-a84c0224bb233a4942301a5075f00dcca596f334.zip |
Add aarch64 ACPI pci-e patches headed for 4.8
Diffstat (limited to 'arm64-pcie-quirks-xgene.patch')
-rw-r--r-- | arm64-pcie-quirks-xgene.patch | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/arm64-pcie-quirks-xgene.patch b/arm64-pcie-quirks-xgene.patch new file mode 100644 index 000000000..8e6805df6 --- /dev/null +++ b/arm64-pcie-quirks-xgene.patch @@ -0,0 +1,508 @@ +From 767b70aa55d013f0c7589955f410d488fed5776a Mon Sep 17 00:00:00 2001 +From: Peter Robinson <pbrobinson@gmail.com> +Date: Tue, 5 Jul 2016 23:49:39 +0100 +Subject: [PATCH 1/4] Some platforms may not be fully compliant with generic + set of PCI config accessors. For these cases we implement the way to + overwrite accessors set. Algorithm traverses available quirk list, matches + against <oem_id, oem_table_id, domain, bus number> tuple and returns + corresponding PCI config ops. oem_id and oem_table_id come from MCFG table + standard header. All quirks can be defined using DECLARE_ACPI_MCFG_FIXUP() + macro and kept self contained. Example: + +/* Custom PCI config ops */ +static struct pci_generic_ecam_ops foo_pci_ops = { + .bus_shift = 24, + .pci_ops = { + .map_bus = pci_ecam_map_bus, + .read = foo_ecam_config_read, + .write = foo_ecam_config_write, + } +}; + +DECLARE_ACPI_MCFG_FIXUP(&foo_pci_ops, <oem_id_str>, <oem_table_id>, <domain_nr>, <bus_nr>); + +Signed-off-by: Tomasz Nowicki <tn@semihalf.com> +Signed-off-by: Dongdong Liu <liudongdong3@huawei.com> +--- + drivers/acpi/pci_mcfg.c | 41 ++++++++++++++++++++++++++++++++++++--- + include/asm-generic/vmlinux.lds.h | 7 +++++++ + include/linux/pci-acpi.h | 20 +++++++++++++++++++ + 3 files changed, 65 insertions(+), 3 deletions(-) + +diff --git a/drivers/acpi/pci_mcfg.c b/drivers/acpi/pci_mcfg.c +index d3c3e85..deb0077 100644 +--- a/drivers/acpi/pci_mcfg.c ++++ b/drivers/acpi/pci_mcfg.c +@@ -22,6 +22,10 @@ + #include <linux/kernel.h> + #include <linux/pci.h> + #include <linux/pci-acpi.h> ++#include <linux/pci-ecam.h> ++ ++/* Root pointer to the mapped MCFG table */ ++static struct acpi_table_mcfg *mcfg_table; + + /* Structure to hold entries from the MCFG table */ + struct mcfg_entry { +@@ -35,6 +39,38 @@ struct mcfg_entry { + /* List to save mcfg entries */ + static LIST_HEAD(pci_mcfg_list); + ++extern struct pci_cfg_fixup __start_acpi_mcfg_fixups[]; ++extern struct pci_cfg_fixup __end_acpi_mcfg_fixups[]; ++ ++struct pci_ecam_ops *pci_mcfg_get_ops(struct acpi_pci_root *root) ++{ ++ int bus_num = root->secondary.start; ++ int domain = root->segment; ++ struct pci_cfg_fixup *f; ++ ++ if (!mcfg_table) ++ return &pci_generic_ecam_ops; ++ ++ /* ++ * Match against platform specific quirks and return corresponding ++ * CAM ops. ++ * ++ * First match against PCI topology <domain:bus> then use OEM ID and ++ * OEM revision from MCFG table standard header. ++ */ ++ for (f = __start_acpi_mcfg_fixups; f < __end_acpi_mcfg_fixups; f++) { ++ if ((f->domain == domain || f->domain == PCI_MCFG_DOMAIN_ANY) && ++ (f->bus_num == bus_num || f->bus_num == PCI_MCFG_BUS_ANY) && ++ (!strncmp(f->oem_id, mcfg_table->header.oem_id, ++ ACPI_OEM_ID_SIZE)) && ++ (!strncmp(f->oem_table_id, mcfg_table->header.oem_table_id, ++ ACPI_OEM_TABLE_ID_SIZE))) ++ return f->ops; ++ } ++ /* No quirks, use ECAM */ ++ return &pci_generic_ecam_ops; ++} ++ + phys_addr_t pci_mcfg_lookup(u16 seg, struct resource *bus_res) + { + struct mcfg_entry *e; +@@ -54,7 +90,6 @@ phys_addr_t pci_mcfg_lookup(u16 seg, struct resource *bus_res) + + static __init int pci_mcfg_parse(struct acpi_table_header *header) + { +- struct acpi_table_mcfg *mcfg; + struct acpi_mcfg_allocation *mptr; + struct mcfg_entry *e, *arr; + int i, n; +@@ -64,8 +99,8 @@ static __init int pci_mcfg_parse(struct acpi_table_header *header) + + n = (header->length - sizeof(struct acpi_table_mcfg)) / + sizeof(struct acpi_mcfg_allocation); +- mcfg = (struct acpi_table_mcfg *)header; +- mptr = (struct acpi_mcfg_allocation *) &mcfg[1]; ++ mcfg_table = (struct acpi_table_mcfg *)header; ++ mptr = (struct acpi_mcfg_allocation *) &mcfg_table[1]; + + arr = kcalloc(n, sizeof(*arr), GFP_KERNEL); + if (!arr) +diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h +index 6a67ab9..43604fc 100644 +--- a/include/asm-generic/vmlinux.lds.h ++++ b/include/asm-generic/vmlinux.lds.h +@@ -300,6 +300,13 @@ + VMLINUX_SYMBOL(__end_pci_fixups_suspend_late) = .; \ + } \ + \ ++ /* ACPI MCFG quirks */ \ ++ .acpi_fixup : AT(ADDR(.acpi_fixup) - LOAD_OFFSET) { \ ++ VMLINUX_SYMBOL(__start_acpi_mcfg_fixups) = .; \ ++ *(.acpi_fixup_mcfg) \ ++ VMLINUX_SYMBOL(__end_acpi_mcfg_fixups) = .; \ ++ } \ ++ \ + /* Built-in firmware blobs */ \ + .builtin_fw : AT(ADDR(.builtin_fw) - LOAD_OFFSET) { \ + VMLINUX_SYMBOL(__start_builtin_fw) = .; \ +diff --git a/include/linux/pci-acpi.h b/include/linux/pci-acpi.h +index 7d63a66..c8a6559 100644 +--- a/include/linux/pci-acpi.h ++++ b/include/linux/pci-acpi.h +@@ -25,6 +25,7 @@ static inline acpi_status pci_acpi_remove_pm_notifier(struct acpi_device *dev) + extern phys_addr_t acpi_pci_root_get_mcfg_addr(acpi_handle handle); + + extern phys_addr_t pci_mcfg_lookup(u16 domain, struct resource *bus_res); ++extern struct pci_ecam_ops *pci_mcfg_get_ops(struct acpi_pci_root *root); + + static inline acpi_handle acpi_find_root_bridge_handle(struct pci_dev *pdev) + { +@@ -72,6 +73,25 @@ struct acpi_pci_root_ops { + int (*prepare_resources)(struct acpi_pci_root_info *info); + }; + ++struct pci_cfg_fixup { ++ struct pci_ecam_ops *ops; ++ char *oem_id; ++ char *oem_table_id; ++ int domain; ++ int bus_num; ++}; ++ ++#define PCI_MCFG_DOMAIN_ANY -1 ++#define PCI_MCFG_BUS_ANY -1 ++ ++/* Designate a routine to fix up buggy MCFG */ ++#define DECLARE_ACPI_MCFG_FIXUP(ops, oem_id, oem_table_id, dom, bus) \ ++ static const struct pci_cfg_fixup \ ++ __mcfg_fixup_##oem_id##oem_table_id##dom##bus \ ++ __used __attribute__((__section__(".acpi_fixup_mcfg"), \ ++ aligned((sizeof(void *))))) = \ ++ { ops, oem_id, oem_table_id, dom, bus }; ++ + extern int acpi_pci_probe_root_resources(struct acpi_pci_root_info *info); + extern struct pci_bus *acpi_pci_root_create(struct acpi_pci_root *root, + struct acpi_pci_root_ops *ops, +-- +2.7.4 + +From 4f86a9b006b25dd7336043dab26058ed6fb2802d Mon Sep 17 00:00:00 2001 +From: Peter Robinson <pbrobinson@gmail.com> +Date: Tue, 5 Jul 2016 23:52:46 +0100 +Subject: [PATCH 2/4] pci_generic_ecam_ops is used by default. Since there are + platforms which have non-compliant ECAM space we need to overwrite these + accessors prior to PCI buses enumeration. In order to do that we call + pci_mcfg_get_ops to retrieve pci_ecam_ops structure so that we can use proper + PCI config space accessors and bus_shift. + +pci_generic_ecam_ops is still used for platforms free from quirks. + +Signed-off-by: Tomasz Nowicki <tn@semihalf.com> +--- + arch/arm64/kernel/pci.c | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/arch/arm64/kernel/pci.c b/arch/arm64/kernel/pci.c +index 94cd43c..a891bda 100644 +--- a/arch/arm64/kernel/pci.c ++++ b/arch/arm64/kernel/pci.c +@@ -139,6 +139,7 @@ pci_acpi_setup_ecam_mapping(struct acpi_pci_root *root) + struct pci_config_window *cfg; + struct resource cfgres; + unsigned int bsz; ++ struct pci_ecam_ops *ops; + + /* Use address from _CBA if present, otherwise lookup MCFG */ + if (!root->mcfg_addr) +@@ -150,12 +151,12 @@ pci_acpi_setup_ecam_mapping(struct acpi_pci_root *root) + return NULL; + } + +- bsz = 1 << pci_generic_ecam_ops.bus_shift; ++ ops = pci_mcfg_get_ops(root); ++ bsz = 1 << ops->bus_shift; + cfgres.start = root->mcfg_addr + bus_res->start * bsz; + cfgres.end = cfgres.start + resource_size(bus_res) * bsz - 1; + cfgres.flags = IORESOURCE_MEM; +- cfg = pci_ecam_create(&root->device->dev, &cfgres, bus_res, +- &pci_generic_ecam_ops); ++ cfg = pci_ecam_create(&root->device->dev, &cfgres, bus_res, ops); + if (IS_ERR(cfg)) { + dev_err(&root->device->dev, "%04x:%pR error %ld mapping ECAM\n", + seg, bus_res, PTR_ERR(cfg)); +-- +2.7.4 + +From cbdbd697bd6d716eb9d1705ee55445432e73eabb Mon Sep 17 00:00:00 2001 +From: Peter Robinson <pbrobinson@gmail.com> +Date: Tue, 5 Jul 2016 23:53:59 +0100 +Subject: [PATCH 3/4] The ECAM quirk matching criteria per the discussion on + https://lkml.org/lkml/2016/6/13/944 includes: OEM ID, OEM Table ID and OEM + Revision. So this patch adds OEM Table ID into the check to match platform + specific ECAM quirks as well. + +This patch also improve strncmp check using strlen and +min_t to ignore the padding spaces in OEM ID and OEM +Table ID. + +Signed-off-by: Duc Dang <dhdang@apm.com> +--- + drivers/acpi/pci_mcfg.c | 7 +++++-- + include/linux/pci-acpi.h | 7 ++++--- + 2 files changed, 9 insertions(+), 5 deletions(-) + +diff --git a/drivers/acpi/pci_mcfg.c b/drivers/acpi/pci_mcfg.c +index deb0077..307ca9a 100644 +--- a/drivers/acpi/pci_mcfg.c ++++ b/drivers/acpi/pci_mcfg.c +@@ -62,9 +62,12 @@ struct pci_ecam_ops *pci_mcfg_get_ops(struct acpi_pci_root *root) + if ((f->domain == domain || f->domain == PCI_MCFG_DOMAIN_ANY) && + (f->bus_num == bus_num || f->bus_num == PCI_MCFG_BUS_ANY) && + (!strncmp(f->oem_id, mcfg_table->header.oem_id, +- ACPI_OEM_ID_SIZE)) && ++ min_t(size_t, strlen(f->oem_id), ++ ACPI_OEM_ID_SIZE))) && + (!strncmp(f->oem_table_id, mcfg_table->header.oem_table_id, +- ACPI_OEM_TABLE_ID_SIZE))) ++ min_t(size_t, strlen(f->oem_table_id), ++ ACPI_OEM_TABLE_ID_SIZE))) && ++ (f->oem_revision == mcfg_table->header.oem_revision)) + return f->ops; + } + /* No quirks, use ECAM */ +diff --git a/include/linux/pci-acpi.h b/include/linux/pci-acpi.h +index c8a6559..5148c8d 100644 +--- a/include/linux/pci-acpi.h ++++ b/include/linux/pci-acpi.h +@@ -77,6 +77,7 @@ struct pci_cfg_fixup { + struct pci_ecam_ops *ops; + char *oem_id; + char *oem_table_id; ++ u32 oem_revision; + int domain; + int bus_num; + }; +@@ -85,12 +86,12 @@ struct pci_cfg_fixup { + #define PCI_MCFG_BUS_ANY -1 + + /* Designate a routine to fix up buggy MCFG */ +-#define DECLARE_ACPI_MCFG_FIXUP(ops, oem_id, oem_table_id, dom, bus) \ ++#define DECLARE_ACPI_MCFG_FIXUP(ops, oem_id, oem_table_id, rev, dom, bus) \ + static const struct pci_cfg_fixup \ +- __mcfg_fixup_##oem_id##oem_table_id##dom##bus \ ++ __mcfg_fixup_##oem_id##oem_table_id##rev##dom##bus \ + __used __attribute__((__section__(".acpi_fixup_mcfg"), \ + aligned((sizeof(void *))))) = \ +- { ops, oem_id, oem_table_id, dom, bus }; ++ { ops, oem_id, oem_table_id, rev, dom, bus }; + + extern int acpi_pci_probe_root_resources(struct acpi_pci_root_info *info); + extern struct pci_bus *acpi_pci_root_create(struct acpi_pci_root *root, +-- +2.7.4 + +From 78766cf255bc6aafac2f57372a0446f78322da19 Mon Sep 17 00:00:00 2001 +From: Peter Robinson <pbrobinson@gmail.com> +Date: Tue, 5 Jul 2016 23:55:11 +0100 +Subject: [PATCH 4/4] X-Gene PCIe controller does not fully support ECAM. This + patch adds required ECAM fixup to allow X-Gene PCIe controller to be + functional in ACPI boot mode. + +Signed-off-by: Duc Dang <dhdang@apm.com> +--- + drivers/pci/host/Makefile | 2 +- + drivers/pci/host/pci-xgene-ecam.c | 194 ++++++++++++++++++++++++++++++++++++++ + 2 files changed, 195 insertions(+), 1 deletion(-) + create mode 100644 drivers/pci/host/pci-xgene-ecam.c + +diff --git a/drivers/pci/host/Makefile b/drivers/pci/host/Makefile +index 9c8698e..3480696 100644 +--- a/drivers/pci/host/Makefile ++++ b/drivers/pci/host/Makefile +@@ -14,7 +14,7 @@ obj-$(CONFIG_PCIE_SPEAR13XX) += pcie-spear13xx.o + obj-$(CONFIG_PCI_KEYSTONE) += pci-keystone-dw.o pci-keystone.o + obj-$(CONFIG_PCIE_XILINX) += pcie-xilinx.o + obj-$(CONFIG_PCIE_XILINX_NWL) += pcie-xilinx-nwl.o +-obj-$(CONFIG_PCI_XGENE) += pci-xgene.o ++obj-$(CONFIG_PCI_XGENE) += pci-xgene.o pci-xgene-ecam.o + obj-$(CONFIG_PCI_XGENE_MSI) += pci-xgene-msi.o + obj-$(CONFIG_PCI_LAYERSCAPE) += pci-layerscape.o + obj-$(CONFIG_PCI_VERSATILE) += pci-versatile.o +diff --git a/drivers/pci/host/pci-xgene-ecam.c b/drivers/pci/host/pci-xgene-ecam.c +new file mode 100644 +index 0000000..1bea63f +--- /dev/null ++++ b/drivers/pci/host/pci-xgene-ecam.c +@@ -0,0 +1,194 @@ ++/* ++ * APM X-Gene PCIe ECAM fixup driver ++ * ++ * Copyright (c) 2016, Applied Micro Circuits Corporation ++ * Author: ++ * Duc Dang <dhdang@apm.com> ++ * ++ * This program is free software; you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License version 2 as ++ * published by the Free Software Foundation. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see <http://www.gnu.org/licenses/>. ++ */ ++ ++#include <linux/kernel.h> ++#include <linux/module.h> ++#include <linux/of_address.h> ++#include <linux/of_pci.h> ++#include <linux/pci-acpi.h> ++#include <linux/platform_device.h> ++#include <linux/pci-ecam.h> ++ ++#ifdef CONFIG_ACPI ++#define RTDID 0x160 ++#define ROOT_CAP_AND_CTRL 0x5C ++ ++/* PCIe IP version */ ++#define XGENE_PCIE_IP_VER_UNKN 0 ++#define XGENE_PCIE_IP_VER_1 1 ++ ++#define APM_OEM_ID "APM" ++#define APM_XGENE_OEM_TABLE_ID "XGENE" ++#define APM_XGENE_OEM_REV 0x00000002 ++ ++struct xgene_pcie_acpi_root { ++ void __iomem *csr_base; ++ u32 version; ++}; ++ ++static acpi_status xgene_pcie_find_csr_base(struct acpi_resource *acpi_res, ++ void *data) ++{ ++ struct xgene_pcie_acpi_root *root = data; ++ struct acpi_resource_fixed_memory32 *fixed32; ++ ++ if (acpi_res->type == ACPI_RESOURCE_TYPE_FIXED_MEMORY32) { ++ fixed32 = &acpi_res->data.fixed_memory32; ++ root->csr_base = ioremap(fixed32->address, ++ fixed32->address_length); ++ return AE_CTRL_TERMINATE; ++ } ++ ++ return AE_OK; ++} ++ ++static int xgene_pcie_ecam_init(struct pci_config_window *cfg) ++{ ++ struct xgene_pcie_acpi_root *xgene_root; ++ struct device *dev = cfg->parent; ++ struct acpi_device *adev = to_acpi_device(dev); ++ acpi_handle handle = acpi_device_handle(adev); ++ ++ xgene_root = devm_kzalloc(dev, sizeof(*xgene_root), GFP_KERNEL); ++ if (!xgene_root) ++ return -ENOMEM; ++ ++ acpi_walk_resources(handle, METHOD_NAME__CRS, ++ xgene_pcie_find_csr_base, xgene_root); ++ ++ if (!xgene_root->csr_base) { ++ kfree(xgene_root); ++ return -ENODEV; ++ } ++ ++ xgene_root->version = XGENE_PCIE_IP_VER_1; ++ ++ cfg->priv = xgene_root; ++ ++ return 0; ++} ++ ++/* ++ * For Configuration request, RTDID register is used as Bus Number, ++ * Device Number and Function number of the header fields. ++ */ ++static void xgene_pcie_set_rtdid_reg(struct pci_bus *bus, uint devfn) ++{ ++ struct pci_config_window *cfg = bus->sysdata; ++ struct xgene_pcie_acpi_root *port = cfg->priv; ++ unsigned int b, d, f; ++ u32 rtdid_val = 0; ++ ++ b = bus->number; ++ d = PCI_SLOT(devfn); ++ f = PCI_FUNC(devfn); ++ ++ if (!pci_is_root_bus(bus)) ++ rtdid_val = (b << 8) | (d << 3) | f; ++ ++ writel(rtdid_val, port->csr_base + RTDID); ++ /* read the register back to ensure flush */ ++ readl(port->csr_base + RTDID); ++} ++ ++/* ++ * X-Gene PCIe port uses BAR0-BAR1 of RC's configuration space as ++ * the translation from PCI bus to native BUS. Entire DDR region ++ * is mapped into PCIe space using these registers, so it can be ++ * reached by DMA from EP devices. The BAR0/1 of bridge should be ++ * hidden during enumeration to avoid the sizing and resource allocation ++ * by PCIe core. ++ */ ++static bool xgene_pcie_hide_rc_bars(struct pci_bus *bus, int offset) ++{ ++ if (pci_is_root_bus(bus) && ((offset == PCI_BASE_ADDRESS_0) || ++ (offset == PCI_BASE_ADDRESS_1))) ++ return true; ++ ++ return false; ++} ++ ++void __iomem *xgene_pcie_ecam_map_bus(struct pci_bus *bus, ++ unsigned int devfn, int where) ++{ ++ struct pci_config_window *cfg = bus->sysdata; ++ unsigned int busn = bus->number; ++ void __iomem *base; ++ ++ if (busn < cfg->busr.start || busn > cfg->busr.end) ++ return NULL; ++ ++ if ((pci_is_root_bus(bus) && devfn != 0) || ++ xgene_pcie_hide_rc_bars(bus, where)) ++ return NULL; ++ ++ xgene_pcie_set_rtdid_reg(bus, devfn); ++ ++ if (busn > cfg->busr.start) ++ base = cfg->win + (1 << cfg->ops->bus_shift); ++ else ++ base = cfg->win; ++ ++ return base + where; ++} ++ ++static int xgene_pcie_config_read32(struct pci_bus *bus, unsigned int devfn, ++ int where, int size, u32 *val) ++{ ++ struct pci_config_window *cfg = bus->sysdata; ++ struct xgene_pcie_acpi_root *port = cfg->priv; ++ ++ if (pci_generic_config_read32(bus, devfn, where & ~0x3, 4, val) != ++ PCIBIOS_SUCCESSFUL) ++ return PCIBIOS_DEVICE_NOT_FOUND; ++ ++ /* ++ * The v1 controller has a bug in its Configuration Request ++ * Retry Status (CRS) logic: when CRS is enabled and we read the ++ * Vendor and Device ID of a non-existent device, the controller ++ * fabricates return data of 0xFFFF0001 ("device exists but is not ++ * ready") instead of 0xFFFFFFFF ("device does not exist"). This ++ * causes the PCI core to retry the read until it times out. ++ * Avoid this by not claiming to support CRS. ++ */ ++ if (pci_is_root_bus(bus) && (port->version == XGENE_PCIE_IP_VER_1) && ++ ((where & ~0x3) == ROOT_CAP_AND_CTRL)) ++ *val &= ~(PCI_EXP_RTCAP_CRSVIS << 16); ++ ++ if (size <= 2) ++ *val = (*val >> (8 * (where & 3))) & ((1 << (size * 8)) - 1); ++ ++ return PCIBIOS_SUCCESSFUL; ++} ++ ++static struct pci_ecam_ops xgene_pcie_ecam_ops = { ++ .bus_shift = 16, ++ .init = xgene_pcie_ecam_init, ++ .pci_ops = { ++ .map_bus = xgene_pcie_ecam_map_bus, ++ .read = xgene_pcie_config_read32, ++ .write = pci_generic_config_write, ++ } ++}; ++ ++DECLARE_ACPI_MCFG_FIXUP(&xgene_pcie_ecam_ops, APM_OEM_ID, ++ APM_XGENE_OEM_TABLE_ID, APM_XGENE_OEM_REV, ++ PCI_MCFG_DOMAIN_ANY, PCI_MCFG_BUS_ANY); ++#endif +-- +2.7.4 + |