| /* SPDX-License-Identifier: GPL-2.0-only */ |
| |
| #include <console/console.h> |
| #include <device/device.h> |
| #include <device/pci.h> |
| #include <device/pci_ids.h> |
| #include <device/mmio.h> |
| #include <device/pci_ops.h> |
| #include <arch/ioapic.h> |
| #include <acpi/acpi.h> |
| #include <cpu/x86/smm.h> |
| #include <bootstate.h> |
| |
| #include <soc/lpc.h> |
| #include <soc/pci_devs.h> |
| #include <soc/ramstage.h> |
| #include <soc/iomap.h> |
| #include <soc/pcr.h> |
| #include <soc/p2sb.h> |
| #include <soc/acpi.h> |
| |
| #include "chip.h" |
| |
| /* PCH-LP redirection entries */ |
| #define PCH_LP_REDIR_ETR 120 |
| |
| /** |
| * Set miscellaneous static southbridge features. |
| * |
| * @param dev PCI device with I/O APIC control registers |
| */ |
| static void pch_enable_ioapic(struct device *dev) |
| { |
| u32 reg32; |
| |
| set_ioapic_id((void *)IO_APIC_ADDR, IO_APIC0); |
| |
| /* affirm full set of redirection table entries ("write once") */ |
| reg32 = io_apic_read((void *)IO_APIC_ADDR, 0x01); |
| |
| reg32 &= ~0x00ff0000; |
| reg32 |= (PCH_LP_REDIR_ETR - 1) << 16; |
| |
| io_apic_write((void *)IO_APIC_ADDR, 0x01, reg32); |
| |
| /* |
| * Select Boot Configuration register (0x03) and |
| * use Processor System Bus (0x01) to deliver interrupts. |
| */ |
| io_apic_write((void *)IO_APIC_ADDR, 0x03, 0x01); |
| } |
| |
| /* interrupt router lookup for internal devices */ |
| struct dnv_ir_lut { |
| /* (dev << 3) | fn */ |
| u8 devfn; |
| u8 ir; |
| }; |
| |
| #define DEVFN(dev, fn) ((dev << 3) | (fn)) |
| |
| static const struct dnv_ir_lut dnv_ir_lut[] = { |
| {.devfn = DEVFN(0x05, 0), .ir = 3}, /* RCEC */ |
| {.devfn = DEVFN(0x06, 0), .ir = 4}, /* Virtual RP to QAT */ |
| {.devfn = DEVFN(0x09, 0), .ir = 7}, /* PCIe RP0 */ |
| {.devfn = DEVFN(0x0a, 0), .ir = 7}, /* PCIe RP1 */ |
| {.devfn = DEVFN(0x0b, 0), .ir = 7}, /* PCIe RP2 */ |
| {.devfn = DEVFN(0x0c, 0), .ir = 7}, /* PCIe RP3 */ |
| {.devfn = DEVFN(0x0e, 0), .ir = 8}, /* PCIe RP4 */ |
| {.devfn = DEVFN(0x0f, 0), .ir = 8}, /* PCIe RP5 */ |
| {.devfn = DEVFN(0x10, 0), .ir = 8}, /* PCIe RP6 */ |
| {.devfn = DEVFN(0x11, 0), .ir = 8}, /* PCIe RP7 */ |
| {.devfn = DEVFN(0x12, 0), .ir = 10}, /* SMBus - Host */ |
| {.devfn = DEVFN(0x13, 0), .ir = 6}, /* AHCI0 */ |
| {.devfn = DEVFN(0x14, 0), .ir = 11}, /* AHCI1 */ |
| {.devfn = DEVFN(0x15, 0), .ir = 9}, /* USB */ |
| {.devfn = DEVFN(0x16, 0), .ir = 1}, /* Virtual RP to LAN0 */ |
| {.devfn = DEVFN(0x17, 0), .ir = 2}, /* Virtual RP to LAN1 */ |
| {.devfn = DEVFN(0x18, 0), .ir = 5}, /* ME HECI1 */ |
| {.devfn = DEVFN(0x18, 1), .ir = 5}, /* ME HECI1 */ |
| {.devfn = DEVFN(0x18, 2), .ir = 5}, /* ME PTIO-IDER */ |
| {.devfn = DEVFN(0x18, 3), .ir = 5}, /* ME PTIO-KT */ |
| {.devfn = DEVFN(0x18, 4), .ir = 5}, /* ME HECI3 */ |
| {.devfn = DEVFN(0x1a, 0), .ir = 10}, /* HSUART0 */ |
| {.devfn = DEVFN(0x1a, 1), .ir = 10}, /* HSUART1 */ |
| {.devfn = DEVFN(0x1a, 2), .ir = 10}, /* HSUART2 */ |
| {.devfn = DEVFN(0x1b, 0), .ir = 12}, /* IE HECI1 */ |
| {.devfn = DEVFN(0x1b, 1), .ir = 12}, /* IE HECI1 */ |
| {.devfn = DEVFN(0x1b, 2), .ir = 12}, /* IE PTIO-IDER */ |
| {.devfn = DEVFN(0x1b, 3), .ir = 12}, /* IE PTIO-KT */ |
| {.devfn = DEVFN(0x1b, 4), .ir = 12}, /* IE HECI3 */ |
| {.devfn = DEVFN(0x1c, 0), .ir = 12}, /* SDHCI */ |
| {.devfn = DEVFN(0x1f, 0), .ir = 0}, /* LPC */ |
| {.devfn = DEVFN(0x1f, 1), .ir = 0}, /* PS2B */ |
| {.devfn = DEVFN(0x1f, 4), .ir = 0}, /* SMBus - Legacy */ |
| {.devfn = DEVFN(0x1f, 7), .ir = 0}, /* Trace Hub */ |
| }; |
| |
| /* |
| * Only 6 of the 8 root ports have swizzling, return '1' if this bdf is one of |
| * them, '0' otherwise |
| */ |
| static int is_dnv_swizzled_rp(uint16_t bdf) |
| { |
| switch (bdf) { |
| case DEVFN(10, 0): |
| case DEVFN(11, 0): |
| case DEVFN(12, 0): |
| case DEVFN(15, 0): |
| case DEVFN(16, 0): |
| case DEVFN(17, 0): |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Figure out which upstream interrupt pin a downstream device gets swizzled to |
| * |
| * config - pointer to chip_info containing routing info |
| * devfn - device/function of root port to check swizzling for |
| * pin - interrupt pin 1-4 = A-D |
| * |
| * Return new pin mapping, 0 if invalid pin |
| */ |
| static int dnv_get_swizzled_pin(config_t *config, u8 devfn, u8 pin) |
| { |
| if (pin < 1 || pin > 4) |
| return 0; |
| |
| devfn >>= 3; |
| if (devfn < 13) |
| devfn -= 9; |
| else |
| devfn -= 14; |
| |
| return ((pin - 1 + devfn) % 4) + 1; |
| } |
| |
| /* |
| * Figure out which upstream interrupt pin a downstream device gets swizzled to |
| * |
| * config - pointer to chip_info containing routing info |
| * devfn - device/function of root port to check swizzling for |
| * pin - interrupt pin 1-4 = A-D |
| * |
| * Return new pin mapping, 0 if invalid pin |
| */ |
| static int dnv_get_ir(config_t *config, u8 devfn, u8 pin) |
| { |
| int i = 0; |
| int line = 0xff; |
| u16 ir = 0xffff; |
| |
| /* The only valid pin values are 1-4 for A-D */ |
| if (pin < 1 || pin > 4) { |
| printk(BIOS_WARNING, "%s: pin %d is invalid\n", __func__, pin); |
| goto dnv_get_ir_done; |
| } |
| |
| for (i = 0; i < ARRAY_SIZE(dnv_ir_lut); i++) { |
| if (dnv_ir_lut[i].devfn == devfn) |
| break; |
| } |
| |
| if (i == ARRAY_SIZE(dnv_ir_lut)) { |
| printk(BIOS_WARNING, "%s: no entry\n", __func__); |
| goto dnv_get_ir_done; |
| } |
| |
| switch (dnv_ir_lut[i].ir) { |
| case 0: |
| ir = config->ir00_routing; |
| break; |
| case 1: |
| ir = config->ir01_routing; |
| break; |
| case 2: |
| ir = config->ir02_routing; |
| break; |
| case 3: |
| ir = config->ir03_routing; |
| break; |
| case 4: |
| ir = config->ir04_routing; |
| break; |
| case 5: |
| ir = config->ir05_routing; |
| break; |
| case 6: |
| ir = config->ir06_routing; |
| break; |
| case 7: |
| ir = config->ir07_routing; |
| break; |
| case 8: |
| ir = config->ir08_routing; |
| break; |
| case 9: |
| ir = config->ir09_routing; |
| break; |
| case 10: |
| ir = config->ir10_routing; |
| break; |
| case 11: |
| ir = config->ir11_routing; |
| break; |
| case 12: |
| ir = config->ir12_routing; |
| break; |
| default: |
| printk(BIOS_ERR, "%s: invalid ir %d for entry %d\n", __func__, dnv_ir_lut[i].ir, |
| i); |
| goto dnv_get_ir_done; |
| } |
| |
| ir >>= (pin - 1) * 4; |
| ir &= 0xf; |
| switch (ir) { |
| case 0: |
| line = config->pirqa_routing; |
| break; |
| case 1: |
| line = config->pirqb_routing; |
| break; |
| case 2: |
| line = config->pirqc_routing; |
| break; |
| case 3: |
| line = config->pirqd_routing; |
| break; |
| case 4: |
| line = config->pirqe_routing; |
| break; |
| case 5: |
| line = config->pirqf_routing; |
| break; |
| case 6: |
| line = config->pirqg_routing; |
| break; |
| case 7: |
| line = config->pirqh_routing; |
| break; |
| default: |
| printk(BIOS_ERR, "%s: invalid ir pirq %d for entry %d\n", __func__, ir, i); |
| break; |
| } |
| |
| dnv_get_ir_done: |
| return line; |
| } |
| |
| /* |
| * PCI devices have the INT_LINE (0x3C) and INT_PIN (0x3D) registers which |
| * report interrupt routing information to operating systems and drivers. The |
| * INT_PIN register is generally read only and reports which interrupt pin |
| * A - D it uses. The INT_LINE register is configurable and reports which IRQ |
| * (generally the PIC IRQs 1 - 15) it will use. This needs to take interrupt |
| * pin swizzling on devices that are downstream on a PCI bridge into account. |
| */ |
| static u8 dnv_get_int_line(struct device *irq_dev) |
| { |
| config_t *config; |
| struct device *targ_dev = NULL; |
| uint16_t parent_bdf = 0; |
| int8_t original_int_pin = 0, new_int_pin = 0, swiz_int_pin = 0; |
| uint8_t int_line = 0xff; |
| |
| if (irq_dev->path.type != DEVICE_PATH_PCI || !irq_dev->enabled) { |
| printk(BIOS_ERR, "%s for non pci device?\n", __func__); |
| goto dnv_get_int_line_done; |
| } |
| |
| /* |
| * Get the INT_PIN swizzled up to the root port if necessary |
| * using the existing coreboot pci_device code |
| */ |
| original_int_pin = pci_read_config8(irq_dev, PCI_INTERRUPT_PIN); |
| new_int_pin = get_pci_irq_pins(irq_dev, &targ_dev); |
| if (targ_dev == NULL || new_int_pin < 1) |
| goto dnv_get_int_line_done; |
| |
| printk(BIOS_DEBUG, "%s: irq_dev %s, targ_dev %s:\n", __func__, dev_path(irq_dev), |
| dev_path(targ_dev)); |
| printk(BIOS_DEBUG, "%s: std swizzle %s from %c to %c\n", __func__, dev_path(targ_dev), |
| '@' + original_int_pin, '@' + new_int_pin); |
| |
| /* Swizzle this device if needed */ |
| config = targ_dev->chip_info; |
| parent_bdf = targ_dev->path.pci.devfn | targ_dev->bus->secondary << 8; |
| if (is_dnv_swizzled_rp(parent_bdf) && irq_dev != targ_dev) { |
| swiz_int_pin = dnv_get_swizzled_pin(config, parent_bdf, new_int_pin); |
| printk(BIOS_DEBUG, "%s: dnv swizzle %s from %c to %c\n", __func__, |
| dev_path(targ_dev), '@' + new_int_pin, '@' + swiz_int_pin); |
| } else { |
| swiz_int_pin = new_int_pin; |
| } |
| |
| /* Look up the routing for the pin */ |
| int_line = dnv_get_ir(config, parent_bdf, swiz_int_pin); |
| |
| dnv_get_int_line_done: |
| printk(BIOS_DEBUG, "\tINT_LINE\t\t: %d\n", int_line); |
| return int_line; |
| } |
| |
| /* PIRQ[n]_ROUT[3:0] - PIRQ Routing Control |
| * 0x00 - 0000 = Reserved |
| * 0x01 - 0001 = Reserved |
| * 0x02 - 0010 = Reserved |
| * 0x03 - 0011 = IRQ3 |
| * 0x04 - 0100 = IRQ4 |
| * 0x05 - 0101 = IRQ5 |
| * 0x06 - 0110 = IRQ6 |
| * 0x07 - 0111 = IRQ7 |
| * 0x08 - 1000 = Reserved |
| * 0x09 - 1001 = IRQ9 |
| * 0x0A - 1010 = IRQ10 |
| * 0x0B - 1011 = IRQ11 |
| * 0x0C - 1100 = IRQ12 |
| * 0x0D - 1101 = Reserved |
| * 0x0E - 1110 = IRQ14 |
| * 0x0F - 1111 = IRQ15 |
| * PIRQ[n]_ROUT[7] - PIRQ Routing Control |
| * 0x80 - The PIRQ is not routed. |
| */ |
| |
| static void pch_pirq_init(struct device *dev) |
| { |
| struct device *irq_dev; |
| /* Get the chip configuration */ |
| config_t *config = config_of(dev); |
| |
| /* Initialize PIRQ Routings */ |
| write8((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIRQA_ROUT), |
| config->pirqa_routing); |
| write8((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIRQB_ROUT), |
| config->pirqb_routing); |
| write8((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIRQC_ROUT), |
| config->pirqc_routing); |
| write8((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIRQD_ROUT), |
| config->pirqd_routing); |
| |
| write8((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIRQE_ROUT), |
| config->pirqe_routing); |
| write8((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIRQF_ROUT), |
| config->pirqf_routing); |
| write8((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIRQG_ROUT), |
| config->pirqg_routing); |
| write8((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIRQH_ROUT), |
| config->pirqh_routing); |
| |
| /* Initialize device's Interrupt Routings */ |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR00), |
| config->ir00_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR01), |
| config->ir01_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR02), |
| config->ir02_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR03), |
| config->ir03_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR04), |
| config->ir04_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR05), |
| config->ir05_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR06), |
| config->ir06_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR07), |
| config->ir07_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR08), |
| config->ir08_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR09), |
| config->ir09_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR10), |
| config->ir10_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR11), |
| config->ir11_routing); |
| write16((void *)PCH_PCR_ADDRESS(PID_ITSS, PCR_ITSS_PIR12), |
| config->ir12_routing); |
| |
| /* Initialize device's Interrupt Polarity Control */ |
| write32((void *)PCH_PCR_ADDRESS(PID_ITSS, PCH_PCR_ITSS_IPC0), |
| config->ipc0); |
| write32((void *)PCH_PCR_ADDRESS(PID_ITSS, PCH_PCR_ITSS_IPC1), |
| config->ipc1); |
| write32((void *)PCH_PCR_ADDRESS(PID_ITSS, PCH_PCR_ITSS_IPC2), |
| config->ipc2); |
| write32((void *)PCH_PCR_ADDRESS(PID_ITSS, PCH_PCR_ITSS_IPC3), |
| config->ipc3); |
| |
| for (irq_dev = all_devices; irq_dev; irq_dev = irq_dev->next) { |
| int devfn = irq_dev->path.pci.devfn; |
| u8 int_pin = 0, int_line = 0; |
| |
| if (!irq_dev->enabled || irq_dev->path.type != DEVICE_PATH_PCI) |
| continue; |
| |
| int_pin = pci_read_config8(irq_dev, PCI_INTERRUPT_PIN); |
| |
| int_line = dnv_get_int_line(irq_dev); |
| printk(BIOS_DEBUG, "%s: %02x:%02x.%d pin %d int line %d\n", __func__, |
| irq_dev->bus->secondary, devfn >> 3, devfn & 0x7, int_pin, int_line); |
| |
| pci_write_config8(irq_dev, PCI_INTERRUPT_LINE, int_line); |
| } |
| } |
| |
| static void pci_p2sb_read_resources(struct device *dev) |
| { |
| struct resource *res; |
| |
| /* Add MMIO resource |
| * Use 0xda as an unused index for PCR BAR. |
| */ |
| res = new_resource(dev, 0xda); |
| res->base = DEFAULT_PCR_BASE; |
| res->size = 16 * 1024 * 1024; /* 16MB PCR config space */ |
| res->flags = IORESOURCE_MEM | IORESOURCE_FIXED | IORESOURCE_STORED | |
| IORESOURCE_ASSIGNED; |
| printk(BIOS_DEBUG, |
| "Adding P2SB PCR config space BAR 0x%08lx-0x%08lx.\n", |
| (unsigned long)(res->base), |
| (unsigned long)(res->base + res->size)); |
| |
| /* Add MMIO resource |
| * Use 0xdb as an unused index for IOAPIC. |
| */ |
| res = new_resource(dev, 0xdb); /* IOAPIC */ |
| res->base = IO_APIC_ADDR; |
| res->size = 0x00001000; |
| res->flags = IORESOURCE_MEM | IORESOURCE_ASSIGNED | IORESOURCE_FIXED; |
| } |
| |
| static void pch_enable_serial_irqs(struct device *dev) |
| { |
| /* Set packet length and toggle silent mode bit for one frame. */ |
| pci_write_config8(dev, SERIRQ_CNTL, |
| (1 << 7) | (1 << 6) | ((21 - 17) << 2) | (0 << 0)); |
| #if !CONFIG(SERIRQ_CONTINUOUS_MODE) |
| pci_write_config8(dev, SERIRQ_CNTL, |
| (1 << 7) | (0 << 6) | ((21 - 17) << 2) | (0 << 0)); |
| #endif |
| } |
| |
| static void lpc_init(struct device *dev) |
| { |
| printk(BIOS_DEBUG, "pch: %s\n", __func__); |
| |
| /* Get the base address */ |
| |
| /* Set the value for PCI command register. */ |
| pci_write_config16(dev, PCI_COMMAND, |
| PCI_COMMAND_SPECIAL | PCI_COMMAND_MASTER | |
| PCI_COMMAND_MEMORY | PCI_COMMAND_IO); |
| |
| /* Serial IRQ initialization. */ |
| pch_enable_serial_irqs(dev); |
| |
| /* IO APIC initialization. */ |
| pch_enable_ioapic(dev); |
| |
| /* Setup the PIRQ. */ |
| pch_pirq_init(dev); |
| } |
| |
| static void pch_lpc_add_mmio_resources(struct device *dev) { /* TODO */ } |
| |
| static void pch_lpc_add_io_resources(struct device *dev) |
| { |
| struct resource *res; |
| u8 io_index = 0; |
| |
| /* Add an extra subtractive resource for both memory and I/O. */ |
| res = new_resource(dev, IOINDEX_SUBTRACTIVE(io_index++, 0)); |
| res->base = 0; |
| res->size = 0x1000; |
| res->flags = IORESOURCE_IO | IORESOURCE_SUBTRACTIVE | |
| IORESOURCE_ASSIGNED | IORESOURCE_FIXED; |
| |
| res = new_resource(dev, IOINDEX_SUBTRACTIVE(io_index++, 0)); |
| res->base = 0xff000000; |
| res->size = 0x01000000; /* 16 MB for flash */ |
| res->flags = IORESOURCE_MEM | IORESOURCE_SUBTRACTIVE | |
| IORESOURCE_ASSIGNED | IORESOURCE_FIXED; |
| } |
| |
| static void lpc_read_resources(struct device *dev) |
| { |
| /* Get the normal PCI resources of this device. */ |
| pci_dev_read_resources(dev); |
| |
| /* Add non-standard MMIO resources. */ |
| pch_lpc_add_mmio_resources(dev); |
| |
| /* Add IO resources. */ |
| pch_lpc_add_io_resources(dev); |
| |
| /* Add MMIO resource for IOAPIC. */ |
| pci_p2sb_read_resources(dev); |
| } |
| |
| static void pch_decode_init(struct device *dev) { /* TODO */ } |
| |
| static void lpc_enable_resources(struct device *dev) |
| { |
| pch_decode_init(dev); |
| pci_dev_enable_resources(dev); |
| } |
| |
| /* Set bit in Function Disable register to hide this device */ |
| static void pch_hide_devfn(uint32_t devfn) { /* TODO */ } |
| |
| void southcluster_enable_dev(struct device *dev) |
| { |
| u16 reg16; |
| |
| if (!dev->enabled) { |
| printk(BIOS_DEBUG, "%s: Disabling device\n", dev_path(dev)); |
| |
| /* Ensure memory, io, and bus master are all disabled */ |
| reg16 = pci_read_config16(dev, PCI_COMMAND); |
| reg16 &= ~(PCI_COMMAND_MASTER | PCI_COMMAND_MEMORY | |
| PCI_COMMAND_IO); |
| pci_write_config16(dev, PCI_COMMAND, reg16); |
| |
| /* Hide this device if possible */ |
| pch_hide_devfn(dev->path.pci.devfn); |
| } else { |
| /* Enable SERR */ |
| pci_or_config16(dev, PCI_COMMAND, PCI_COMMAND_SERR); |
| } |
| } |
| |
| static struct device_operations device_ops = { |
| .read_resources = lpc_read_resources, |
| .set_resources = pci_dev_set_resources, |
| #if CONFIG(HAVE_ACPI_TABLES) |
| .write_acpi_tables = southcluster_write_acpi_tables, |
| #endif |
| .enable_resources = lpc_enable_resources, |
| .init = lpc_init, |
| .enable = southcluster_enable_dev, |
| .scan_bus = scan_static_bus, |
| .ops_pci = &soc_pci_ops, |
| }; |
| |
| static const struct pci_driver lpc_driver __pci_driver = { |
| .ops = &device_ops, |
| .vendor = PCI_VENDOR_ID_INTEL, |
| .device = PCI_DEVICE_ID_INTEL_DENVERTON_LPC, |
| }; |
| |
| static void finalize_chipset(void *unused) |
| { |
| apm_control(APM_CNT_FINALIZE); |
| } |
| |
| BOOT_STATE_INIT_ENTRY(BS_OS_RESUME, BS_ON_ENTRY, finalize_chipset, NULL); |
| BOOT_STATE_INIT_ENTRY(BS_PAYLOAD_LOAD, BS_ON_EXIT, finalize_chipset, NULL); |