mirror of https://gitee.com/openkylin/qemu.git
target-arm:
* new "unimplemented" device for stubbing out devices in a system model so accesses can be logged * stellaris: document the SoC memory map * arm: create instruction syndromes for AArch32 data aborts * arm: Correctly handle watchpoints for BE32 CPUs * Fix Thumb-1 BE32 execution and disassembly * arm: Add cfgend parameter for ARM CPU selection * sd: sdhci: check data length during dma_memory_read * aspeed: add a watchdog controller * integratorcp: adding vmstate for save/restore -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 iQIcBAABCAAGBQJYmh3zAAoJEDwlJe0UNgzeZF0P/0v1AomIyNLaJ8Xyr6/9Ia9L xn8jYsACWyT3AQ4BXdmaGXg24+wPVT4X36wzTNS50s4a3R4VWxaqu+9bxAKVtyfH Y1CIXIib5Gz9FyY1n5pvpj7b438h4QtNbgvb/0vJoECk2F99QjEQxX+XJlCGGAGi J8O/L2cfrzZUXLfRZQW1VfcIkgLvlGo1A489/MFYQuv+irzJckHmnY3LeqrRfKCB udYbaaAdtCiPQ/OlCzsDXucjOXBMqrGba+f3g+P4xmtDPYTlSvvOjFroR54v9xa8 n8qLLelEiH8uhW3vl8aeEsdPMV3yrX2yz0HzMqhzXTajJJs8wxECtvYKWaSzosKP 64pbSlHD8iwl1oHJ+ZOlwaCpAcaVnBLwz65VSB6EUxAGPpeFqp0q6uATc7t9dO3o PRAijt5IrNpKHehTmcZ1bAsO59KeCBFwMyGL8clX2jD1Vjj8zD9CrkCDL4tpJlsL uGHn9RyywsWcMGsjpP6JXo3uYMK1Wbozt0XRnWWex+wsW3qOdCHVGIhXyyDJBLMD +r+hc49//GA58GtdTDVsU/qI4NgLfVSp2qk4lKlYVu1kxp5azPs2OpRn44Hv6YID 2VmepX4ug/pfJ0y2AHyYuJEO+auHKzunjDCrnrBqQEMrON2hmvqtHnS/G496+lIG 146ctV+2sSPz2vxOvTwf =dbln -----END PGP SIGNATURE----- Merge remote-tracking branch 'remotes/pmaydell/tags/pull-target-arm-20170207-1' into staging target-arm: * new "unimplemented" device for stubbing out devices in a system model so accesses can be logged * stellaris: document the SoC memory map * arm: create instruction syndromes for AArch32 data aborts * arm: Correctly handle watchpoints for BE32 CPUs * Fix Thumb-1 BE32 execution and disassembly * arm: Add cfgend parameter for ARM CPU selection * sd: sdhci: check data length during dma_memory_read * aspeed: add a watchdog controller * integratorcp: adding vmstate for save/restore # gpg: Signature made Tue 07 Feb 2017 19:20:19 GMT # gpg: using RSA key 0x3C2525ED14360CDE # gpg: Good signature from "Peter Maydell <peter.maydell@linaro.org>" # gpg: aka "Peter Maydell <pmaydell@gmail.com>" # gpg: aka "Peter Maydell <pmaydell@chiark.greenend.org.uk>" # Primary key fingerprint: E1A5 C593 CD41 9DE2 8E83 15CF 3C25 25ED 1436 0CDE * remotes/pmaydell/tags/pull-target-arm-20170207-1: stellaris: Use the 'unimplemented' device for parts we don't implement hw/misc: New "unimplemented" sysbus device stellaris: Document memory map and which SoC devices are unimplemented target/arm: A32, T32: Create Instruction Syndromes for Data Aborts target/arm: Abstract out pbit/wbit tests in ARM ldr/str decode arm: Correctly handle watchpoints for BE32 CPUs Fix Thumb-1 BE32 execution and disassembly. target/arm: Add cfgend parameter for ARM CPU selection. hw/arm/integratorcp: Support specifying features via -cpu sd: sdhci: check data length during dma_memory_read aspeed: add a watchdog controller wdt: Add Aspeed watchdog device model integratorcp: adding vmstate for save/restore Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
commit
f073cd3a2b
1
disas.c
1
disas.c
|
@ -190,6 +190,7 @@ void target_disas(FILE *out, CPUState *cpu, target_ulong code,
|
|||
|
||||
s.cpu = cpu;
|
||||
s.info.read_memory_func = target_read_memory;
|
||||
s.info.read_memory_inner_func = NULL;
|
||||
s.info.buffer_vma = code;
|
||||
s.info.buffer_length = size;
|
||||
s.info.print_address_func = generic_print_address;
|
||||
|
|
1
exec.c
1
exec.c
|
@ -2115,6 +2115,7 @@ static void check_watchpoint(int offset, int len, MemTxAttrs attrs, int flags)
|
|||
return;
|
||||
}
|
||||
vaddr = (cpu->mem_io_vaddr & TARGET_PAGE_MASK) + offset;
|
||||
vaddr = cc->adjust_watchpoint_address(cpu, vaddr, len);
|
||||
QTAILQ_FOREACH(wp, &cpu->watchpoints, entry) {
|
||||
if (cpu_watchpoint_address_matches(wp, vaddr, len)
|
||||
&& (wp->flags & flags)) {
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
#define ASPEED_SOC_SCU_BASE 0x1E6E2000
|
||||
#define ASPEED_SOC_SRAM_BASE 0x1E720000
|
||||
#define ASPEED_SOC_TIMER_BASE 0x1E782000
|
||||
#define ASPEED_SOC_WDT_BASE 0x1E785000
|
||||
#define ASPEED_SOC_I2C_BASE 0x1E78A000
|
||||
|
||||
static const int uart_irqs[] = { 9, 32, 33, 34, 10 };
|
||||
|
@ -170,6 +171,10 @@ static void aspeed_soc_init(Object *obj)
|
|||
sc->info->silicon_rev);
|
||||
object_property_add_alias(obj, "ram-size", OBJECT(&s->sdmc),
|
||||
"ram-size", &error_abort);
|
||||
|
||||
object_initialize(&s->wdt, sizeof(s->wdt), TYPE_ASPEED_WDT);
|
||||
object_property_add_child(obj, "wdt", OBJECT(&s->wdt), NULL);
|
||||
qdev_set_parent_bus(DEVICE(&s->wdt), sysbus_get_default());
|
||||
}
|
||||
|
||||
static void aspeed_soc_realize(DeviceState *dev, Error **errp)
|
||||
|
@ -286,6 +291,14 @@ static void aspeed_soc_realize(DeviceState *dev, Error **errp)
|
|||
return;
|
||||
}
|
||||
sysbus_mmio_map(SYS_BUS_DEVICE(&s->sdmc), 0, ASPEED_SOC_SDMC_BASE);
|
||||
|
||||
/* Watch dog */
|
||||
object_property_set_bool(OBJECT(&s->wdt), true, "realized", &err);
|
||||
if (err) {
|
||||
error_propagate(errp, err);
|
||||
return;
|
||||
}
|
||||
sysbus_mmio_map(SYS_BUS_DEVICE(&s->wdt), 0, ASPEED_SOC_WDT_BASE);
|
||||
}
|
||||
|
||||
static void aspeed_soc_class_init(ObjectClass *oc, void *data)
|
||||
|
|
|
@ -53,6 +53,26 @@ static uint8_t integrator_spd[128] = {
|
|||
0xe, 4, 0x1c, 1, 2, 0x20, 0xc0, 0, 0, 0, 0, 0x30, 0x28, 0x30, 0x28, 0x40
|
||||
};
|
||||
|
||||
static const VMStateDescription vmstate_integratorcm = {
|
||||
.name = "integratorcm",
|
||||
.version_id = 1,
|
||||
.minimum_version_id = 1,
|
||||
.fields = (VMStateField[]) {
|
||||
VMSTATE_UINT32(cm_osc, IntegratorCMState),
|
||||
VMSTATE_UINT32(cm_ctrl, IntegratorCMState),
|
||||
VMSTATE_UINT32(cm_lock, IntegratorCMState),
|
||||
VMSTATE_UINT32(cm_auxosc, IntegratorCMState),
|
||||
VMSTATE_UINT32(cm_sdram, IntegratorCMState),
|
||||
VMSTATE_UINT32(cm_init, IntegratorCMState),
|
||||
VMSTATE_UINT32(cm_flags, IntegratorCMState),
|
||||
VMSTATE_UINT32(cm_nvflags, IntegratorCMState),
|
||||
VMSTATE_UINT32(int_level, IntegratorCMState),
|
||||
VMSTATE_UINT32(irq_enabled, IntegratorCMState),
|
||||
VMSTATE_UINT32(fiq_enabled, IntegratorCMState),
|
||||
VMSTATE_END_OF_LIST()
|
||||
}
|
||||
};
|
||||
|
||||
static uint64_t integratorcm_read(void *opaque, hwaddr offset,
|
||||
unsigned size)
|
||||
{
|
||||
|
@ -309,6 +329,18 @@ typedef struct icp_pic_state {
|
|||
qemu_irq parent_fiq;
|
||||
} icp_pic_state;
|
||||
|
||||
static const VMStateDescription vmstate_icp_pic = {
|
||||
.name = "icp_pic",
|
||||
.version_id = 1,
|
||||
.minimum_version_id = 1,
|
||||
.fields = (VMStateField[]) {
|
||||
VMSTATE_UINT32(level, icp_pic_state),
|
||||
VMSTATE_UINT32(irq_enabled, icp_pic_state),
|
||||
VMSTATE_UINT32(fiq_enabled, icp_pic_state),
|
||||
VMSTATE_END_OF_LIST()
|
||||
}
|
||||
};
|
||||
|
||||
static void icp_pic_update(icp_pic_state *s)
|
||||
{
|
||||
uint32_t flags;
|
||||
|
@ -438,6 +470,16 @@ typedef struct ICPCtrlRegsState {
|
|||
#define ICP_INTREG_WPROT (1 << 0)
|
||||
#define ICP_INTREG_CARDIN (1 << 3)
|
||||
|
||||
static const VMStateDescription vmstate_icp_control = {
|
||||
.name = "icp_control",
|
||||
.version_id = 1,
|
||||
.minimum_version_id = 1,
|
||||
.fields = (VMStateField[]) {
|
||||
VMSTATE_UINT32(intreg_state, ICPCtrlRegsState),
|
||||
VMSTATE_END_OF_LIST()
|
||||
}
|
||||
};
|
||||
|
||||
static uint64_t icp_control_read(void *opaque, hwaddr offset,
|
||||
unsigned size)
|
||||
{
|
||||
|
@ -535,27 +577,42 @@ static void integratorcp_init(MachineState *machine)
|
|||
const char *kernel_filename = machine->kernel_filename;
|
||||
const char *kernel_cmdline = machine->kernel_cmdline;
|
||||
const char *initrd_filename = machine->initrd_filename;
|
||||
char **cpustr;
|
||||
ObjectClass *cpu_oc;
|
||||
CPUClass *cc;
|
||||
Object *cpuobj;
|
||||
ARMCPU *cpu;
|
||||
const char *typename;
|
||||
MemoryRegion *address_space_mem = get_system_memory();
|
||||
MemoryRegion *ram = g_new(MemoryRegion, 1);
|
||||
MemoryRegion *ram_alias = g_new(MemoryRegion, 1);
|
||||
qemu_irq pic[32];
|
||||
DeviceState *dev, *sic, *icp;
|
||||
int i;
|
||||
Error *err = NULL;
|
||||
|
||||
if (!cpu_model) {
|
||||
cpu_model = "arm926";
|
||||
}
|
||||
|
||||
cpu_oc = cpu_class_by_name(TYPE_ARM_CPU, cpu_model);
|
||||
cpustr = g_strsplit(cpu_model, ",", 2);
|
||||
|
||||
cpu_oc = cpu_class_by_name(TYPE_ARM_CPU, cpustr[0]);
|
||||
if (!cpu_oc) {
|
||||
fprintf(stderr, "Unable to find CPU definition\n");
|
||||
exit(1);
|
||||
}
|
||||
typename = object_class_get_name(cpu_oc);
|
||||
|
||||
cpuobj = object_new(object_class_get_name(cpu_oc));
|
||||
cc = CPU_CLASS(cpu_oc);
|
||||
cc->parse_features(typename, cpustr[1], &err);
|
||||
g_strfreev(cpustr);
|
||||
if (err) {
|
||||
error_report_err(err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
cpuobj = object_new(typename);
|
||||
|
||||
/* By default ARM1176 CPUs have EL3 enabled. This board does not
|
||||
* currently support EL3 so the CPU EL3 property is disabled before
|
||||
|
@ -640,6 +697,21 @@ static void core_class_init(ObjectClass *klass, void *data)
|
|||
|
||||
dc->props = core_properties;
|
||||
dc->realize = integratorcm_realize;
|
||||
dc->vmsd = &vmstate_integratorcm;
|
||||
}
|
||||
|
||||
static void icp_pic_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
|
||||
dc->vmsd = &vmstate_icp_pic;
|
||||
}
|
||||
|
||||
static void icp_control_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
|
||||
dc->vmsd = &vmstate_icp_control;
|
||||
}
|
||||
|
||||
static const TypeInfo core_info = {
|
||||
|
@ -655,6 +727,7 @@ static const TypeInfo icp_pic_info = {
|
|||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(icp_pic_state),
|
||||
.instance_init = icp_pic_init,
|
||||
.class_init = icp_pic_class_init,
|
||||
};
|
||||
|
||||
static const TypeInfo icp_ctrl_regs_info = {
|
||||
|
@ -662,6 +735,7 @@ static const TypeInfo icp_ctrl_regs_info = {
|
|||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(ICPCtrlRegsState),
|
||||
.instance_init = icp_control_init,
|
||||
.class_init = icp_control_class_init,
|
||||
};
|
||||
|
||||
static void integratorcp_register_types(void)
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include "exec/address-spaces.h"
|
||||
#include "sysemu/sysemu.h"
|
||||
#include "hw/char/pl011.h"
|
||||
#include "hw/misc/unimp.h"
|
||||
|
||||
#define GPIO_A 0
|
||||
#define GPIO_B 1
|
||||
|
@ -1220,6 +1221,40 @@ static void stellaris_init(const char *kernel_filename, const char *cpu_model,
|
|||
0x40024000, 0x40025000, 0x40026000};
|
||||
static const int gpio_irq[7] = {0, 1, 2, 3, 4, 30, 31};
|
||||
|
||||
/* Memory map of SoC devices, from
|
||||
* Stellaris LM3S6965 Microcontroller Data Sheet (rev I)
|
||||
* http://www.ti.com/lit/ds/symlink/lm3s6965.pdf
|
||||
*
|
||||
* 40000000 wdtimer (unimplemented)
|
||||
* 40002000 i2c (unimplemented)
|
||||
* 40004000 GPIO
|
||||
* 40005000 GPIO
|
||||
* 40006000 GPIO
|
||||
* 40007000 GPIO
|
||||
* 40008000 SSI
|
||||
* 4000c000 UART
|
||||
* 4000d000 UART
|
||||
* 4000e000 UART
|
||||
* 40020000 i2c
|
||||
* 40021000 i2c (unimplemented)
|
||||
* 40024000 GPIO
|
||||
* 40025000 GPIO
|
||||
* 40026000 GPIO
|
||||
* 40028000 PWM (unimplemented)
|
||||
* 4002c000 QEI (unimplemented)
|
||||
* 4002d000 QEI (unimplemented)
|
||||
* 40030000 gptimer
|
||||
* 40031000 gptimer
|
||||
* 40032000 gptimer
|
||||
* 40033000 gptimer
|
||||
* 40038000 ADC
|
||||
* 4003c000 analogue comparator (unimplemented)
|
||||
* 40048000 ethernet
|
||||
* 400fc000 hibernation module (unimplemented)
|
||||
* 400fd000 flash memory control (unimplemented)
|
||||
* 400fe000 system control
|
||||
*/
|
||||
|
||||
DeviceState *gpio_dev[7], *nvic;
|
||||
qemu_irq gpio_in[7][8];
|
||||
qemu_irq gpio_out[7][8];
|
||||
|
@ -1370,6 +1405,19 @@ static void stellaris_init(const char *kernel_filename, const char *cpu_model,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Add dummy regions for the devices we don't implement yet,
|
||||
* so guest accesses don't cause unlogged crashes.
|
||||
*/
|
||||
create_unimplemented_device("wdtimer", 0x40000000, 0x1000);
|
||||
create_unimplemented_device("i2c-0", 0x40002000, 0x1000);
|
||||
create_unimplemented_device("i2c-2", 0x40021000, 0x1000);
|
||||
create_unimplemented_device("PWM", 0x40028000, 0x1000);
|
||||
create_unimplemented_device("QEI-0", 0x4002c000, 0x1000);
|
||||
create_unimplemented_device("QEI-1", 0x4002d000, 0x1000);
|
||||
create_unimplemented_device("analogue-comparator", 0x4003c000, 0x1000);
|
||||
create_unimplemented_device("hibernation", 0x400fc000, 0x1000);
|
||||
create_unimplemented_device("flash-control", 0x400fd000, 0x1000);
|
||||
}
|
||||
|
||||
/* FIXME: Figure out how to generate these from stellaris_boards. */
|
||||
|
|
|
@ -6,6 +6,8 @@ common-obj-$(CONFIG_SGA) += sga.o
|
|||
common-obj-$(CONFIG_ISA_TESTDEV) += pc-testdev.o
|
||||
common-obj-$(CONFIG_PCI_TESTDEV) += pci-testdev.o
|
||||
|
||||
common-obj-y += unimp.o
|
||||
|
||||
obj-$(CONFIG_VMPORT) += vmport.o
|
||||
|
||||
# ARM devices
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/* "Unimplemented" device
|
||||
*
|
||||
* This is a dummy device which accepts and logs all accesses.
|
||||
* It's useful for stubbing out regions of an SoC or board
|
||||
* map which correspond to devices that have not yet been
|
||||
* implemented. This is often sufficient to placate initial
|
||||
* guest device driver probing such that the system will
|
||||
* come up.
|
||||
*
|
||||
* Copyright Linaro Limited, 2017
|
||||
* Written by Peter Maydell
|
||||
*/
|
||||
|
||||
#include "qemu/osdep.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/misc/unimp.h"
|
||||
#include "qemu/log.h"
|
||||
#include "qapi/error.h"
|
||||
|
||||
#define UNIMPLEMENTED_DEVICE(obj) \
|
||||
OBJECT_CHECK(UnimplementedDeviceState, (obj), TYPE_UNIMPLEMENTED_DEVICE)
|
||||
|
||||
typedef struct {
|
||||
SysBusDevice parent_obj;
|
||||
MemoryRegion iomem;
|
||||
char *name;
|
||||
uint64_t size;
|
||||
} UnimplementedDeviceState;
|
||||
|
||||
static uint64_t unimp_read(void *opaque, hwaddr offset, unsigned size)
|
||||
{
|
||||
UnimplementedDeviceState *s = UNIMPLEMENTED_DEVICE(opaque);
|
||||
|
||||
qemu_log_mask(LOG_UNIMP, "%s: unimplemented device read "
|
||||
"(size %d, offset 0x%" HWADDR_PRIx ")\n",
|
||||
s->name, size, offset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void unimp_write(void *opaque, hwaddr offset,
|
||||
uint64_t value, unsigned size)
|
||||
{
|
||||
UnimplementedDeviceState *s = UNIMPLEMENTED_DEVICE(opaque);
|
||||
|
||||
qemu_log_mask(LOG_UNIMP, "%s: unimplemented device write "
|
||||
"(size %d, value 0x%" PRIx64
|
||||
", offset 0x%" HWADDR_PRIx ")\n",
|
||||
s->name, size, value, offset);
|
||||
}
|
||||
|
||||
static const MemoryRegionOps unimp_ops = {
|
||||
.read = unimp_read,
|
||||
.write = unimp_write,
|
||||
.impl.min_access_size = 1,
|
||||
.impl.max_access_size = 8,
|
||||
.valid.min_access_size = 1,
|
||||
.valid.max_access_size = 8,
|
||||
.endianness = DEVICE_NATIVE_ENDIAN,
|
||||
};
|
||||
|
||||
static void unimp_realize(DeviceState *dev, Error **errp)
|
||||
{
|
||||
UnimplementedDeviceState *s = UNIMPLEMENTED_DEVICE(dev);
|
||||
|
||||
if (s->size == 0) {
|
||||
error_setg(errp, "property 'size' not specified or zero");
|
||||
return;
|
||||
}
|
||||
|
||||
if (s->name == NULL) {
|
||||
error_setg(errp, "property 'name' not specified");
|
||||
return;
|
||||
}
|
||||
|
||||
memory_region_init_io(&s->iomem, OBJECT(s), &unimp_ops, s,
|
||||
s->name, s->size);
|
||||
sysbus_init_mmio(SYS_BUS_DEVICE(s), &s->iomem);
|
||||
}
|
||||
|
||||
static Property unimp_properties[] = {
|
||||
DEFINE_PROP_UINT64("size", UnimplementedDeviceState, size, 0),
|
||||
DEFINE_PROP_STRING("name", UnimplementedDeviceState, name),
|
||||
DEFINE_PROP_END_OF_LIST(),
|
||||
};
|
||||
|
||||
static void unimp_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
|
||||
dc->realize = unimp_realize;
|
||||
dc->props = unimp_properties;
|
||||
}
|
||||
|
||||
static const TypeInfo unimp_info = {
|
||||
.name = TYPE_UNIMPLEMENTED_DEVICE,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(UnimplementedDeviceState),
|
||||
.class_init = unimp_class_init,
|
||||
};
|
||||
|
||||
static void unimp_register_types(void)
|
||||
{
|
||||
type_register_static(&unimp_info);
|
||||
}
|
||||
|
||||
type_init(unimp_register_types)
|
|
@ -536,7 +536,7 @@ static void sdhci_sdma_transfer_multi_blocks(SDHCIState *s)
|
|||
boundary_count -= block_size - begin;
|
||||
}
|
||||
dma_memory_read(&address_space_memory, s->sdmasysad,
|
||||
&s->fifo_buffer[begin], s->data_count);
|
||||
&s->fifo_buffer[begin], s->data_count - begin);
|
||||
s->sdmasysad += s->data_count - begin;
|
||||
if (s->data_count == block_size) {
|
||||
for (n = 0; n < block_size; n++) {
|
||||
|
|
|
@ -2,3 +2,4 @@ common-obj-y += watchdog.o
|
|||
common-obj-$(CONFIG_WDT_IB6300ESB) += wdt_i6300esb.o
|
||||
common-obj-$(CONFIG_WDT_IB700) += wdt_ib700.o
|
||||
common-obj-$(CONFIG_WDT_DIAG288) += wdt_diag288.o
|
||||
common-obj-$(CONFIG_ASPEED_SOC) += wdt_aspeed.o
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* ASPEED Watchdog Controller
|
||||
*
|
||||
* Copyright (C) 2016-2017 IBM Corp.
|
||||
*
|
||||
* This code is licensed under the GPL version 2 or later. See the
|
||||
* COPYING file in the top-level directory.
|
||||
*/
|
||||
|
||||
#include "qemu/osdep.h"
|
||||
#include "qemu/log.h"
|
||||
#include "sysemu/watchdog.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "qemu/timer.h"
|
||||
#include "hw/watchdog/wdt_aspeed.h"
|
||||
|
||||
#define WDT_STATUS (0x00 / 4)
|
||||
#define WDT_RELOAD_VALUE (0x04 / 4)
|
||||
#define WDT_RESTART (0x08 / 4)
|
||||
#define WDT_CTRL (0x0C / 4)
|
||||
#define WDT_CTRL_RESET_MODE_SOC (0x00 << 5)
|
||||
#define WDT_CTRL_RESET_MODE_FULL_CHIP (0x01 << 5)
|
||||
#define WDT_CTRL_1MHZ_CLK BIT(4)
|
||||
#define WDT_CTRL_WDT_EXT BIT(3)
|
||||
#define WDT_CTRL_WDT_INTR BIT(2)
|
||||
#define WDT_CTRL_RESET_SYSTEM BIT(1)
|
||||
#define WDT_CTRL_ENABLE BIT(0)
|
||||
|
||||
#define WDT_TIMEOUT_STATUS (0x10 / 4)
|
||||
#define WDT_TIMEOUT_CLEAR (0x14 / 4)
|
||||
#define WDT_RESET_WDITH (0x18 / 4)
|
||||
|
||||
#define WDT_RESTART_MAGIC 0x4755
|
||||
|
||||
static bool aspeed_wdt_is_enabled(const AspeedWDTState *s)
|
||||
{
|
||||
return s->regs[WDT_CTRL] & WDT_CTRL_ENABLE;
|
||||
}
|
||||
|
||||
static uint64_t aspeed_wdt_read(void *opaque, hwaddr offset, unsigned size)
|
||||
{
|
||||
AspeedWDTState *s = ASPEED_WDT(opaque);
|
||||
|
||||
offset >>= 2;
|
||||
|
||||
switch (offset) {
|
||||
case WDT_STATUS:
|
||||
return s->regs[WDT_STATUS];
|
||||
case WDT_RELOAD_VALUE:
|
||||
return s->regs[WDT_RELOAD_VALUE];
|
||||
case WDT_RESTART:
|
||||
qemu_log_mask(LOG_GUEST_ERROR,
|
||||
"%s: read from write-only reg at offset 0x%"
|
||||
HWADDR_PRIx "\n", __func__, offset);
|
||||
return 0;
|
||||
case WDT_CTRL:
|
||||
return s->regs[WDT_CTRL];
|
||||
case WDT_TIMEOUT_STATUS:
|
||||
case WDT_TIMEOUT_CLEAR:
|
||||
case WDT_RESET_WDITH:
|
||||
qemu_log_mask(LOG_UNIMP,
|
||||
"%s: uninmplemented read at offset 0x%" HWADDR_PRIx "\n",
|
||||
__func__, offset);
|
||||
return 0;
|
||||
default:
|
||||
qemu_log_mask(LOG_GUEST_ERROR,
|
||||
"%s: Out-of-bounds read at offset 0x%" HWADDR_PRIx "\n",
|
||||
__func__, offset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void aspeed_wdt_reload(AspeedWDTState *s, bool pclk)
|
||||
{
|
||||
uint32_t reload;
|
||||
|
||||
if (pclk) {
|
||||
reload = muldiv64(s->regs[WDT_RELOAD_VALUE], NANOSECONDS_PER_SECOND,
|
||||
s->pclk_freq);
|
||||
} else {
|
||||
reload = s->regs[WDT_RELOAD_VALUE] * 1000;
|
||||
}
|
||||
|
||||
if (aspeed_wdt_is_enabled(s)) {
|
||||
timer_mod(s->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + reload);
|
||||
}
|
||||
}
|
||||
|
||||
static void aspeed_wdt_write(void *opaque, hwaddr offset, uint64_t data,
|
||||
unsigned size)
|
||||
{
|
||||
AspeedWDTState *s = ASPEED_WDT(opaque);
|
||||
bool enable = data & WDT_CTRL_ENABLE;
|
||||
|
||||
offset >>= 2;
|
||||
|
||||
switch (offset) {
|
||||
case WDT_STATUS:
|
||||
qemu_log_mask(LOG_GUEST_ERROR,
|
||||
"%s: write to read-only reg at offset 0x%"
|
||||
HWADDR_PRIx "\n", __func__, offset);
|
||||
break;
|
||||
case WDT_RELOAD_VALUE:
|
||||
s->regs[WDT_RELOAD_VALUE] = data;
|
||||
break;
|
||||
case WDT_RESTART:
|
||||
if ((data & 0xFFFF) == WDT_RESTART_MAGIC) {
|
||||
s->regs[WDT_STATUS] = s->regs[WDT_RELOAD_VALUE];
|
||||
aspeed_wdt_reload(s, !(data & WDT_CTRL_1MHZ_CLK));
|
||||
}
|
||||
break;
|
||||
case WDT_CTRL:
|
||||
if (enable && !aspeed_wdt_is_enabled(s)) {
|
||||
s->regs[WDT_CTRL] = data;
|
||||
aspeed_wdt_reload(s, !(data & WDT_CTRL_1MHZ_CLK));
|
||||
} else if (!enable && aspeed_wdt_is_enabled(s)) {
|
||||
s->regs[WDT_CTRL] = data;
|
||||
timer_del(s->timer);
|
||||
}
|
||||
break;
|
||||
case WDT_TIMEOUT_STATUS:
|
||||
case WDT_TIMEOUT_CLEAR:
|
||||
case WDT_RESET_WDITH:
|
||||
qemu_log_mask(LOG_UNIMP,
|
||||
"%s: uninmplemented write at offset 0x%" HWADDR_PRIx "\n",
|
||||
__func__, offset);
|
||||
break;
|
||||
default:
|
||||
qemu_log_mask(LOG_GUEST_ERROR,
|
||||
"%s: Out-of-bounds write at offset 0x%" HWADDR_PRIx "\n",
|
||||
__func__, offset);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
static WatchdogTimerModel model = {
|
||||
.wdt_name = TYPE_ASPEED_WDT,
|
||||
.wdt_description = "Aspeed watchdog device",
|
||||
};
|
||||
|
||||
static const VMStateDescription vmstate_aspeed_wdt = {
|
||||
.name = "vmstate_aspeed_wdt",
|
||||
.version_id = 0,
|
||||
.minimum_version_id = 0,
|
||||
.fields = (VMStateField[]) {
|
||||
VMSTATE_TIMER_PTR(timer, AspeedWDTState),
|
||||
VMSTATE_UINT32_ARRAY(regs, AspeedWDTState, ASPEED_WDT_REGS_MAX),
|
||||
VMSTATE_END_OF_LIST()
|
||||
}
|
||||
};
|
||||
|
||||
static const MemoryRegionOps aspeed_wdt_ops = {
|
||||
.read = aspeed_wdt_read,
|
||||
.write = aspeed_wdt_write,
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
.valid.min_access_size = 4,
|
||||
.valid.max_access_size = 4,
|
||||
.valid.unaligned = false,
|
||||
};
|
||||
|
||||
static void aspeed_wdt_reset(DeviceState *dev)
|
||||
{
|
||||
AspeedWDTState *s = ASPEED_WDT(dev);
|
||||
|
||||
s->regs[WDT_STATUS] = 0x3EF1480;
|
||||
s->regs[WDT_RELOAD_VALUE] = 0x03EF1480;
|
||||
s->regs[WDT_RESTART] = 0;
|
||||
s->regs[WDT_CTRL] = 0;
|
||||
|
||||
timer_del(s->timer);
|
||||
}
|
||||
|
||||
static void aspeed_wdt_timer_expired(void *dev)
|
||||
{
|
||||
AspeedWDTState *s = ASPEED_WDT(dev);
|
||||
|
||||
qemu_log_mask(CPU_LOG_RESET, "Watchdog timer expired.\n");
|
||||
watchdog_perform_action();
|
||||
timer_del(s->timer);
|
||||
}
|
||||
|
||||
#define PCLK_HZ 24000000
|
||||
|
||||
static void aspeed_wdt_realize(DeviceState *dev, Error **errp)
|
||||
{
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
|
||||
AspeedWDTState *s = ASPEED_WDT(dev);
|
||||
|
||||
s->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, aspeed_wdt_timer_expired, dev);
|
||||
|
||||
/* FIXME: This setting should be derived from the SCU hw strapping
|
||||
* register SCU70
|
||||
*/
|
||||
s->pclk_freq = PCLK_HZ;
|
||||
|
||||
memory_region_init_io(&s->iomem, OBJECT(s), &aspeed_wdt_ops, s,
|
||||
TYPE_ASPEED_WDT, ASPEED_WDT_REGS_MAX * 4);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
}
|
||||
|
||||
static void aspeed_wdt_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
|
||||
dc->realize = aspeed_wdt_realize;
|
||||
dc->reset = aspeed_wdt_reset;
|
||||
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
|
||||
dc->vmsd = &vmstate_aspeed_wdt;
|
||||
}
|
||||
|
||||
static const TypeInfo aspeed_wdt_info = {
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.name = TYPE_ASPEED_WDT,
|
||||
.instance_size = sizeof(AspeedWDTState),
|
||||
.class_init = aspeed_wdt_class_init,
|
||||
};
|
||||
|
||||
static void wdt_aspeed_register_types(void)
|
||||
{
|
||||
watchdog_add_model(&model);
|
||||
type_register_static(&aspeed_wdt_info);
|
||||
}
|
||||
|
||||
type_init(wdt_aspeed_register_types)
|
|
@ -295,6 +295,7 @@ typedef struct disassemble_info {
|
|||
The bottom 16 bits are for the internal use of the disassembler. */
|
||||
unsigned long flags;
|
||||
#define INSN_HAS_RELOC 0x80000000
|
||||
#define INSN_ARM_BE32 0x00010000
|
||||
PTR private_data;
|
||||
|
||||
/* Function used to get bytes to disassemble. MEMADDR is the
|
||||
|
@ -306,6 +307,12 @@ typedef struct disassemble_info {
|
|||
(bfd_vma memaddr, bfd_byte *myaddr, int length,
|
||||
struct disassemble_info *info);
|
||||
|
||||
/* A place to stash the real read_memory_func if read_memory_func wants to
|
||||
do some funky address arithmetic or similar (e.g. for ARM BE32 mode). */
|
||||
int (*read_memory_inner_func)
|
||||
(bfd_vma memaddr, bfd_byte *myaddr, int length,
|
||||
struct disassemble_info *info);
|
||||
|
||||
/* Function which should be called if we get an error that we can't
|
||||
recover from. STATUS is the errno value from read_memory_func and
|
||||
MEMADDR is the address that we were trying to read. INFO is a
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "hw/timer/aspeed_timer.h"
|
||||
#include "hw/i2c/aspeed_i2c.h"
|
||||
#include "hw/ssi/aspeed_smc.h"
|
||||
#include "hw/watchdog/wdt_aspeed.h"
|
||||
|
||||
#define ASPEED_SPIS_NUM 2
|
||||
|
||||
|
@ -37,6 +38,7 @@ typedef struct AspeedSoCState {
|
|||
AspeedSMCState fmc;
|
||||
AspeedSMCState spi[ASPEED_SPIS_NUM];
|
||||
AspeedSDMCState sdmc;
|
||||
AspeedWDTState wdt;
|
||||
} AspeedSoCState;
|
||||
|
||||
#define TYPE_ASPEED_SOC "aspeed-soc"
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* "Unimplemented" device
|
||||
*
|
||||
* Copyright Linaro Limited, 2017
|
||||
* Written by Peter Maydell
|
||||
*/
|
||||
|
||||
#ifndef HW_MISC_UNIMP_H
|
||||
#define HW_MISC_UNIMP_H
|
||||
|
||||
#define TYPE_UNIMPLEMENTED_DEVICE "unimplemented-device"
|
||||
|
||||
/**
|
||||
* create_unimplemented_device: create and map a dummy device
|
||||
* @name: name of the device for debug logging
|
||||
* @base: base address of the device's MMIO region
|
||||
* @size: size of the device's MMIO region
|
||||
*
|
||||
* This utility function creates and maps an instance of unimplemented-device,
|
||||
* which is a dummy device which simply logs all guest accesses to
|
||||
* it via the qemu_log LOG_UNIMP debug log.
|
||||
* The device is mapped at priority -1000, which means that you can
|
||||
* use it to cover a large region and then map other devices on top of it
|
||||
* if necessary.
|
||||
*/
|
||||
static inline void create_unimplemented_device(const char *name,
|
||||
hwaddr base,
|
||||
hwaddr size)
|
||||
{
|
||||
DeviceState *dev = qdev_create(NULL, TYPE_UNIMPLEMENTED_DEVICE);
|
||||
|
||||
qdev_prop_set_string(dev, "name", name);
|
||||
qdev_prop_set_uint64(dev, "size", size);
|
||||
qdev_init_nofail(dev);
|
||||
|
||||
sysbus_mmio_map_overlap(SYS_BUS_DEVICE(dev), 0, base, -1000);
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* ASPEED Watchdog Controller
|
||||
*
|
||||
* Copyright (C) 2016-2017 IBM Corp.
|
||||
*
|
||||
* This code is licensed under the GPL version 2 or later. See the
|
||||
* COPYING file in the top-level directory.
|
||||
*/
|
||||
#ifndef ASPEED_WDT_H
|
||||
#define ASPEED_WDT_H
|
||||
|
||||
#include "hw/sysbus.h"
|
||||
|
||||
#define TYPE_ASPEED_WDT "aspeed.wdt"
|
||||
#define ASPEED_WDT(obj) \
|
||||
OBJECT_CHECK(AspeedWDTState, (obj), TYPE_ASPEED_WDT)
|
||||
|
||||
#define ASPEED_WDT_REGS_MAX (0x20 / 4)
|
||||
|
||||
typedef struct AspeedWDTState {
|
||||
/*< private >*/
|
||||
SysBusDevice parent_obj;
|
||||
QEMUTimer *timer;
|
||||
|
||||
/*< public >*/
|
||||
MemoryRegion iomem;
|
||||
uint32_t regs[ASPEED_WDT_REGS_MAX];
|
||||
|
||||
uint32_t pclk_freq;
|
||||
} AspeedWDTState;
|
||||
|
||||
#endif /* ASPEED_WDT_H */
|
|
@ -132,6 +132,8 @@ struct TranslationBlock;
|
|||
* @cpu_exec_exit: Callback for cpu_exec cleanup.
|
||||
* @cpu_exec_interrupt: Callback for processing interrupts in cpu_exec.
|
||||
* @disas_set_info: Setup architecture specific components of disassembly info
|
||||
* @adjust_watchpoint_address: Perform a target-specific adjustment to an
|
||||
* address before attempting to match it against watchpoints.
|
||||
*
|
||||
* Represents a CPU family or model.
|
||||
*/
|
||||
|
@ -195,6 +197,7 @@ typedef struct CPUClass {
|
|||
bool (*cpu_exec_interrupt)(CPUState *cpu, int interrupt_request);
|
||||
|
||||
void (*disas_set_info)(CPUState *cpu, disassemble_info *info);
|
||||
vaddr (*adjust_watchpoint_address)(CPUState *cpu, vaddr addr, int len);
|
||||
} CPUClass;
|
||||
|
||||
#ifdef HOST_WORDS_BIGENDIAN
|
||||
|
|
|
@ -391,6 +391,11 @@ static int64_t cpu_common_get_arch_id(CPUState *cpu)
|
|||
return cpu->cpu_index;
|
||||
}
|
||||
|
||||
static vaddr cpu_adjust_watchpoint_address(CPUState *cpu, vaddr addr, int len)
|
||||
{
|
||||
return addr;
|
||||
}
|
||||
|
||||
static void cpu_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
|
@ -415,6 +420,7 @@ static void cpu_class_init(ObjectClass *klass, void *data)
|
|||
k->cpu_exec_enter = cpu_common_noop;
|
||||
k->cpu_exec_exit = cpu_common_noop;
|
||||
k->cpu_exec_interrupt = cpu_common_exec_interrupt;
|
||||
k->adjust_watchpoint_address = cpu_adjust_watchpoint_address;
|
||||
set_bit(DEVICE_CATEGORY_CPU, dc->categories);
|
||||
dc->realize = cpu_common_realizefn;
|
||||
dc->unrealize = cpu_common_unrealizefn;
|
||||
|
|
|
@ -39,7 +39,15 @@ static inline uint32_t arm_ldl_code(CPUARMState *env, target_ulong addr,
|
|||
static inline uint16_t arm_lduw_code(CPUARMState *env, target_ulong addr,
|
||||
bool sctlr_b)
|
||||
{
|
||||
uint16_t insn = cpu_lduw_code(env, addr);
|
||||
uint16_t insn;
|
||||
#ifndef CONFIG_USER_ONLY
|
||||
/* In big-endian (BE32) mode, adjacent Thumb instructions have been swapped
|
||||
within each word. Undo that now. */
|
||||
if (sctlr_b) {
|
||||
addr ^= 2;
|
||||
}
|
||||
#endif
|
||||
insn = cpu_lduw_code(env, addr);
|
||||
if (bswap_code(sctlr_b)) {
|
||||
return bswap16(insn);
|
||||
}
|
||||
|
|
|
@ -446,6 +446,21 @@ print_insn_thumb1(bfd_vma pc, disassemble_info *info)
|
|||
return print_insn_arm(pc | 1, info);
|
||||
}
|
||||
|
||||
static int arm_read_memory_func(bfd_vma memaddr, bfd_byte *b,
|
||||
int length, struct disassemble_info *info)
|
||||
{
|
||||
assert(info->read_memory_inner_func);
|
||||
assert((info->flags & INSN_ARM_BE32) == 0 || length == 2 || length == 4);
|
||||
|
||||
if ((info->flags & INSN_ARM_BE32) != 0 && length == 2) {
|
||||
assert(info->endian == BFD_ENDIAN_LITTLE);
|
||||
return info->read_memory_inner_func(memaddr ^ 2, (bfd_byte *)b, 2,
|
||||
info);
|
||||
} else {
|
||||
return info->read_memory_inner_func(memaddr, b, length, info);
|
||||
}
|
||||
}
|
||||
|
||||
static void arm_disas_set_info(CPUState *cpu, disassemble_info *info)
|
||||
{
|
||||
ARMCPU *ac = ARM_CPU(cpu);
|
||||
|
@ -471,6 +486,14 @@ static void arm_disas_set_info(CPUState *cpu, disassemble_info *info)
|
|||
info->endian = BFD_ENDIAN_BIG;
|
||||
#endif
|
||||
}
|
||||
if (info->read_memory_inner_func == NULL) {
|
||||
info->read_memory_inner_func = info->read_memory_func;
|
||||
info->read_memory_func = arm_read_memory_func;
|
||||
}
|
||||
info->flags &= ~INSN_ARM_BE32;
|
||||
if (arm_sctlr_b(env)) {
|
||||
info->flags |= INSN_ARM_BE32;
|
||||
}
|
||||
}
|
||||
|
||||
static void arm_cpu_initfn(Object *obj)
|
||||
|
@ -541,6 +564,9 @@ static Property arm_cpu_has_el2_property =
|
|||
static Property arm_cpu_has_el3_property =
|
||||
DEFINE_PROP_BOOL("has_el3", ARMCPU, has_el3, true);
|
||||
|
||||
static Property arm_cpu_cfgend_property =
|
||||
DEFINE_PROP_BOOL("cfgend", ARMCPU, cfgend, false);
|
||||
|
||||
/* use property name "pmu" to match other archs and virt tools */
|
||||
static Property arm_cpu_has_pmu_property =
|
||||
DEFINE_PROP_BOOL("pmu", ARMCPU, has_pmu, true);
|
||||
|
@ -608,6 +634,8 @@ static void arm_cpu_post_init(Object *obj)
|
|||
}
|
||||
}
|
||||
|
||||
qdev_property_add_static(DEVICE(obj), &arm_cpu_cfgend_property,
|
||||
&error_abort);
|
||||
}
|
||||
|
||||
static void arm_cpu_finalizefn(Object *obj)
|
||||
|
@ -728,6 +756,14 @@ static void arm_cpu_realizefn(DeviceState *dev, Error **errp)
|
|||
cpu->reset_sctlr |= (1 << 13);
|
||||
}
|
||||
|
||||
if (cpu->cfgend) {
|
||||
if (arm_feature(&cpu->env, ARM_FEATURE_V7)) {
|
||||
cpu->reset_sctlr |= SCTLR_EE;
|
||||
} else {
|
||||
cpu->reset_sctlr |= SCTLR_B;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cpu->has_el3) {
|
||||
/* If the has_el3 CPU property is disabled then we need to disable the
|
||||
* feature.
|
||||
|
@ -1639,6 +1675,9 @@ static void arm_cpu_class_init(ObjectClass *oc, void *data)
|
|||
cc->gdb_stop_before_watchpoint = true;
|
||||
cc->debug_excp_handler = arm_debug_excp_handler;
|
||||
cc->debug_check_watchpoint = arm_debug_check_watchpoint;
|
||||
#if !defined(CONFIG_USER_ONLY)
|
||||
cc->adjust_watchpoint_address = arm_adjust_watchpoint_address;
|
||||
#endif
|
||||
|
||||
cc->disas_set_info = arm_disas_set_info;
|
||||
}
|
||||
|
|
|
@ -676,6 +676,13 @@ struct ARMCPU {
|
|||
int gic_vpribits; /* number of virtual priority bits */
|
||||
int gic_vprebits; /* number of virtual preemption bits */
|
||||
|
||||
/* Whether the cfgend input is high (i.e. this CPU should reset into
|
||||
* big-endian mode). This setting isn't used directly: instead it modifies
|
||||
* the reset_sctlr value to have SCTLR_B or SCTLR_EE set, depending on the
|
||||
* architecture version.
|
||||
*/
|
||||
bool cfgend;
|
||||
|
||||
ARMELChangeHook *el_change_hook;
|
||||
void *el_change_hook_opaque;
|
||||
};
|
||||
|
|
|
@ -444,6 +444,11 @@ void hw_breakpoint_update_all(ARMCPU *cpu);
|
|||
/* Callback function for checking if a watchpoint should trigger. */
|
||||
bool arm_debug_check_watchpoint(CPUState *cs, CPUWatchpoint *wp);
|
||||
|
||||
/* Adjust addresses (in BE32 mode) before testing against watchpoint
|
||||
* addresses.
|
||||
*/
|
||||
vaddr arm_adjust_watchpoint_address(CPUState *cs, vaddr addr, int len);
|
||||
|
||||
/* Callback function for when a watchpoint or breakpoint triggers. */
|
||||
void arm_debug_excp_handler(CPUState *cs);
|
||||
|
||||
|
|
|
@ -1225,6 +1225,28 @@ bool arm_debug_check_watchpoint(CPUState *cs, CPUWatchpoint *wp)
|
|||
return check_watchpoints(cpu);
|
||||
}
|
||||
|
||||
vaddr arm_adjust_watchpoint_address(CPUState *cs, vaddr addr, int len)
|
||||
{
|
||||
ARMCPU *cpu = ARM_CPU(cs);
|
||||
CPUARMState *env = &cpu->env;
|
||||
|
||||
/* In BE32 system mode, target memory is stored byteswapped (on a
|
||||
* little-endian host system), and by the time we reach here (via an
|
||||
* opcode helper) the addresses of subword accesses have been adjusted
|
||||
* to account for that, which means that watchpoints will not match.
|
||||
* Undo the adjustment here.
|
||||
*/
|
||||
if (arm_sctlr_b(env)) {
|
||||
if (len == 1) {
|
||||
addr ^= 3;
|
||||
} else if (len == 2) {
|
||||
addr ^= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
void arm_debug_excp_handler(CPUState *cs)
|
||||
{
|
||||
/* Called by core code when a watchpoint or breakpoint fires;
|
||||
|
|
|
@ -379,20 +379,6 @@ static inline void gen_goto_tb(DisasContext *s, int n, uint64_t dest)
|
|||
}
|
||||
}
|
||||
|
||||
static void disas_set_insn_syndrome(DisasContext *s, uint32_t syn)
|
||||
{
|
||||
/* We don't need to save all of the syndrome so we mask and shift
|
||||
* out uneeded bits to help the sleb128 encoder do a better job.
|
||||
*/
|
||||
syn &= ARM_INSN_START_WORD2_MASK;
|
||||
syn >>= ARM_INSN_START_WORD2_SHIFT;
|
||||
|
||||
/* We check and clear insn_start_idx to catch multiple updates. */
|
||||
assert(s->insn_start_idx != 0);
|
||||
tcg_set_insn_param(s->insn_start_idx, 2, syn);
|
||||
s->insn_start_idx = 0;
|
||||
}
|
||||
|
||||
static void unallocated_encoding(DisasContext *s)
|
||||
{
|
||||
/* Unallocated and reserved encodings are uncategorized */
|
||||
|
|
|
@ -102,6 +102,49 @@ void arm_translate_init(void)
|
|||
a64_translate_init();
|
||||
}
|
||||
|
||||
/* Flags for the disas_set_da_iss info argument:
|
||||
* lower bits hold the Rt register number, higher bits are flags.
|
||||
*/
|
||||
typedef enum ISSInfo {
|
||||
ISSNone = 0,
|
||||
ISSRegMask = 0x1f,
|
||||
ISSInvalid = (1 << 5),
|
||||
ISSIsAcqRel = (1 << 6),
|
||||
ISSIsWrite = (1 << 7),
|
||||
ISSIs16Bit = (1 << 8),
|
||||
} ISSInfo;
|
||||
|
||||
/* Save the syndrome information for a Data Abort */
|
||||
static void disas_set_da_iss(DisasContext *s, TCGMemOp memop, ISSInfo issinfo)
|
||||
{
|
||||
uint32_t syn;
|
||||
int sas = memop & MO_SIZE;
|
||||
bool sse = memop & MO_SIGN;
|
||||
bool is_acqrel = issinfo & ISSIsAcqRel;
|
||||
bool is_write = issinfo & ISSIsWrite;
|
||||
bool is_16bit = issinfo & ISSIs16Bit;
|
||||
int srt = issinfo & ISSRegMask;
|
||||
|
||||
if (issinfo & ISSInvalid) {
|
||||
/* Some callsites want to conditionally provide ISS info,
|
||||
* eg "only if this was not a writeback"
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
if (srt == 15) {
|
||||
/* For AArch32, insns where the src/dest is R15 never generate
|
||||
* ISS information. Catching that here saves checking at all
|
||||
* the call sites.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
syn = syn_data_abort_with_iss(0, sas, sse, srt, 0, is_acqrel,
|
||||
0, 0, 0, is_write, 0, is_16bit);
|
||||
disas_set_insn_syndrome(s, syn);
|
||||
}
|
||||
|
||||
static inline ARMMMUIdx get_a32_user_mem_index(DisasContext *s)
|
||||
{
|
||||
/* Return the mmu_idx to use for A32/T32 "unprivileged load/store"
|
||||
|
@ -933,6 +976,14 @@ static inline void gen_aa32_ld##SUFF(DisasContext *s, TCGv_i32 val, \
|
|||
TCGv_i32 a32, int index) \
|
||||
{ \
|
||||
gen_aa32_ld_i32(s, val, a32, index, OPC | s->be_data); \
|
||||
} \
|
||||
static inline void gen_aa32_ld##SUFF##_iss(DisasContext *s, \
|
||||
TCGv_i32 val, \
|
||||
TCGv_i32 a32, int index, \
|
||||
ISSInfo issinfo) \
|
||||
{ \
|
||||
gen_aa32_ld##SUFF(s, val, a32, index); \
|
||||
disas_set_da_iss(s, OPC, issinfo); \
|
||||
}
|
||||
|
||||
#define DO_GEN_ST(SUFF, OPC) \
|
||||
|
@ -940,6 +991,14 @@ static inline void gen_aa32_st##SUFF(DisasContext *s, TCGv_i32 val, \
|
|||
TCGv_i32 a32, int index) \
|
||||
{ \
|
||||
gen_aa32_st_i32(s, val, a32, index, OPC | s->be_data); \
|
||||
} \
|
||||
static inline void gen_aa32_st##SUFF##_iss(DisasContext *s, \
|
||||
TCGv_i32 val, \
|
||||
TCGv_i32 a32, int index, \
|
||||
ISSInfo issinfo) \
|
||||
{ \
|
||||
gen_aa32_st##SUFF(s, val, a32, index); \
|
||||
disas_set_da_iss(s, OPC, issinfo | ISSIsWrite); \
|
||||
}
|
||||
|
||||
static inline void gen_aa32_frob64(DisasContext *s, TCGv_i64 val)
|
||||
|
@ -8682,16 +8741,19 @@ static void disas_arm_insn(DisasContext *s, unsigned int insn)
|
|||
tmp = tcg_temp_new_i32();
|
||||
switch (op1) {
|
||||
case 0: /* lda */
|
||||
gen_aa32_ld32u(s, tmp, addr,
|
||||
get_mem_index(s));
|
||||
gen_aa32_ld32u_iss(s, tmp, addr,
|
||||
get_mem_index(s),
|
||||
rd | ISSIsAcqRel);
|
||||
break;
|
||||
case 2: /* ldab */
|
||||
gen_aa32_ld8u(s, tmp, addr,
|
||||
get_mem_index(s));
|
||||
gen_aa32_ld8u_iss(s, tmp, addr,
|
||||
get_mem_index(s),
|
||||
rd | ISSIsAcqRel);
|
||||
break;
|
||||
case 3: /* ldah */
|
||||
gen_aa32_ld16u(s, tmp, addr,
|
||||
get_mem_index(s));
|
||||
gen_aa32_ld16u_iss(s, tmp, addr,
|
||||
get_mem_index(s),
|
||||
rd | ISSIsAcqRel);
|
||||
break;
|
||||
default:
|
||||
abort();
|
||||
|
@ -8702,16 +8764,19 @@ static void disas_arm_insn(DisasContext *s, unsigned int insn)
|
|||
tmp = load_reg(s, rm);
|
||||
switch (op1) {
|
||||
case 0: /* stl */
|
||||
gen_aa32_st32(s, tmp, addr,
|
||||
get_mem_index(s));
|
||||
gen_aa32_st32_iss(s, tmp, addr,
|
||||
get_mem_index(s),
|
||||
rm | ISSIsAcqRel);
|
||||
break;
|
||||
case 2: /* stlb */
|
||||
gen_aa32_st8(s, tmp, addr,
|
||||
get_mem_index(s));
|
||||
gen_aa32_st8_iss(s, tmp, addr,
|
||||
get_mem_index(s),
|
||||
rm | ISSIsAcqRel);
|
||||
break;
|
||||
case 3: /* stlh */
|
||||
gen_aa32_st16(s, tmp, addr,
|
||||
get_mem_index(s));
|
||||
gen_aa32_st16_iss(s, tmp, addr,
|
||||
get_mem_index(s),
|
||||
rm | ISSIsAcqRel);
|
||||
break;
|
||||
default:
|
||||
abort();
|
||||
|
@ -8782,11 +8847,18 @@ static void disas_arm_insn(DisasContext *s, unsigned int insn)
|
|||
} else {
|
||||
int address_offset;
|
||||
bool load = insn & (1 << 20);
|
||||
bool wbit = insn & (1 << 21);
|
||||
bool pbit = insn & (1 << 24);
|
||||
bool doubleword = false;
|
||||
ISSInfo issinfo;
|
||||
|
||||
/* Misc load/store */
|
||||
rn = (insn >> 16) & 0xf;
|
||||
rd = (insn >> 12) & 0xf;
|
||||
|
||||
/* ISS not valid if writeback */
|
||||
issinfo = (pbit & !wbit) ? rd : ISSInvalid;
|
||||
|
||||
if (!load && (sh & 2)) {
|
||||
/* doubleword */
|
||||
ARCH(5TE);
|
||||
|
@ -8799,8 +8871,9 @@ static void disas_arm_insn(DisasContext *s, unsigned int insn)
|
|||
}
|
||||
|
||||
addr = load_reg(s, rn);
|
||||
if (insn & (1 << 24))
|
||||
if (pbit) {
|
||||
gen_add_datah_offset(s, insn, 0, addr);
|
||||
}
|
||||
address_offset = 0;
|
||||
|
||||
if (doubleword) {
|
||||
|
@ -8829,30 +8902,33 @@ static void disas_arm_insn(DisasContext *s, unsigned int insn)
|
|||
tmp = tcg_temp_new_i32();
|
||||
switch (sh) {
|
||||
case 1:
|
||||
gen_aa32_ld16u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld16u_iss(s, tmp, addr, get_mem_index(s),
|
||||
issinfo);
|
||||
break;
|
||||
case 2:
|
||||
gen_aa32_ld8s(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld8s_iss(s, tmp, addr, get_mem_index(s),
|
||||
issinfo);
|
||||
break;
|
||||
default:
|
||||
case 3:
|
||||
gen_aa32_ld16s(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld16s_iss(s, tmp, addr, get_mem_index(s),
|
||||
issinfo);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
/* store */
|
||||
tmp = load_reg(s, rd);
|
||||
gen_aa32_st16(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st16_iss(s, tmp, addr, get_mem_index(s), issinfo);
|
||||
tcg_temp_free_i32(tmp);
|
||||
}
|
||||
/* Perform base writeback before the loaded value to
|
||||
ensure correct behavior with overlapping index registers.
|
||||
ldrd with base writeback is undefined if the
|
||||
destination and index registers overlap. */
|
||||
if (!(insn & (1 << 24))) {
|
||||
if (!pbit) {
|
||||
gen_add_datah_offset(s, insn, address_offset, addr);
|
||||
store_reg(s, rn, addr);
|
||||
} else if (insn & (1 << 21)) {
|
||||
} else if (wbit) {
|
||||
if (address_offset)
|
||||
tcg_gen_addi_i32(addr, addr, address_offset);
|
||||
store_reg(s, rn, addr);
|
||||
|
@ -9195,17 +9271,17 @@ static void disas_arm_insn(DisasContext *s, unsigned int insn)
|
|||
/* load */
|
||||
tmp = tcg_temp_new_i32();
|
||||
if (insn & (1 << 22)) {
|
||||
gen_aa32_ld8u(s, tmp, tmp2, i);
|
||||
gen_aa32_ld8u_iss(s, tmp, tmp2, i, rd);
|
||||
} else {
|
||||
gen_aa32_ld32u(s, tmp, tmp2, i);
|
||||
gen_aa32_ld32u_iss(s, tmp, tmp2, i, rd);
|
||||
}
|
||||
} else {
|
||||
/* store */
|
||||
tmp = load_reg(s, rd);
|
||||
if (insn & (1 << 22)) {
|
||||
gen_aa32_st8(s, tmp, tmp2, i);
|
||||
gen_aa32_st8_iss(s, tmp, tmp2, i, rd);
|
||||
} else {
|
||||
gen_aa32_st32(s, tmp, tmp2, i);
|
||||
gen_aa32_st32_iss(s, tmp, tmp2, i, rd);
|
||||
}
|
||||
tcg_temp_free_i32(tmp);
|
||||
}
|
||||
|
@ -9666,13 +9742,16 @@ static int disas_thumb2_insn(CPUARMState *env, DisasContext *s, uint16_t insn_hw
|
|||
tmp = tcg_temp_new_i32();
|
||||
switch (op) {
|
||||
case 0: /* ldab */
|
||||
gen_aa32_ld8u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld8u_iss(s, tmp, addr, get_mem_index(s),
|
||||
rs | ISSIsAcqRel);
|
||||
break;
|
||||
case 1: /* ldah */
|
||||
gen_aa32_ld16u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld16u_iss(s, tmp, addr, get_mem_index(s),
|
||||
rs | ISSIsAcqRel);
|
||||
break;
|
||||
case 2: /* lda */
|
||||
gen_aa32_ld32u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld32u_iss(s, tmp, addr, get_mem_index(s),
|
||||
rs | ISSIsAcqRel);
|
||||
break;
|
||||
default:
|
||||
abort();
|
||||
|
@ -9682,13 +9761,16 @@ static int disas_thumb2_insn(CPUARMState *env, DisasContext *s, uint16_t insn_hw
|
|||
tmp = load_reg(s, rs);
|
||||
switch (op) {
|
||||
case 0: /* stlb */
|
||||
gen_aa32_st8(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st8_iss(s, tmp, addr, get_mem_index(s),
|
||||
rs | ISSIsAcqRel);
|
||||
break;
|
||||
case 1: /* stlh */
|
||||
gen_aa32_st16(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st16_iss(s, tmp, addr, get_mem_index(s),
|
||||
rs | ISSIsAcqRel);
|
||||
break;
|
||||
case 2: /* stl */
|
||||
gen_aa32_st32(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st32_iss(s, tmp, addr, get_mem_index(s),
|
||||
rs | ISSIsAcqRel);
|
||||
break;
|
||||
default:
|
||||
abort();
|
||||
|
@ -10634,6 +10716,8 @@ static int disas_thumb2_insn(CPUARMState *env, DisasContext *s, uint16_t insn_hw
|
|||
int postinc = 0;
|
||||
int writeback = 0;
|
||||
int memidx;
|
||||
ISSInfo issinfo;
|
||||
|
||||
if ((insn & 0x01100000) == 0x01000000) {
|
||||
if (disas_neon_ls_insn(s, insn)) {
|
||||
goto illegal_op;
|
||||
|
@ -10737,24 +10821,27 @@ static int disas_thumb2_insn(CPUARMState *env, DisasContext *s, uint16_t insn_hw
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
issinfo = writeback ? ISSInvalid : rs;
|
||||
|
||||
if (insn & (1 << 20)) {
|
||||
/* Load. */
|
||||
tmp = tcg_temp_new_i32();
|
||||
switch (op) {
|
||||
case 0:
|
||||
gen_aa32_ld8u(s, tmp, addr, memidx);
|
||||
gen_aa32_ld8u_iss(s, tmp, addr, memidx, issinfo);
|
||||
break;
|
||||
case 4:
|
||||
gen_aa32_ld8s(s, tmp, addr, memidx);
|
||||
gen_aa32_ld8s_iss(s, tmp, addr, memidx, issinfo);
|
||||
break;
|
||||
case 1:
|
||||
gen_aa32_ld16u(s, tmp, addr, memidx);
|
||||
gen_aa32_ld16u_iss(s, tmp, addr, memidx, issinfo);
|
||||
break;
|
||||
case 5:
|
||||
gen_aa32_ld16s(s, tmp, addr, memidx);
|
||||
gen_aa32_ld16s_iss(s, tmp, addr, memidx, issinfo);
|
||||
break;
|
||||
case 2:
|
||||
gen_aa32_ld32u(s, tmp, addr, memidx);
|
||||
gen_aa32_ld32u_iss(s, tmp, addr, memidx, issinfo);
|
||||
break;
|
||||
default:
|
||||
tcg_temp_free_i32(tmp);
|
||||
|
@ -10771,13 +10858,13 @@ static int disas_thumb2_insn(CPUARMState *env, DisasContext *s, uint16_t insn_hw
|
|||
tmp = load_reg(s, rs);
|
||||
switch (op) {
|
||||
case 0:
|
||||
gen_aa32_st8(s, tmp, addr, memidx);
|
||||
gen_aa32_st8_iss(s, tmp, addr, memidx, issinfo);
|
||||
break;
|
||||
case 1:
|
||||
gen_aa32_st16(s, tmp, addr, memidx);
|
||||
gen_aa32_st16_iss(s, tmp, addr, memidx, issinfo);
|
||||
break;
|
||||
case 2:
|
||||
gen_aa32_st32(s, tmp, addr, memidx);
|
||||
gen_aa32_st32_iss(s, tmp, addr, memidx, issinfo);
|
||||
break;
|
||||
default:
|
||||
tcg_temp_free_i32(tmp);
|
||||
|
@ -10914,7 +11001,8 @@ static void disas_thumb_insn(CPUARMState *env, DisasContext *s)
|
|||
addr = tcg_temp_new_i32();
|
||||
tcg_gen_movi_i32(addr, val);
|
||||
tmp = tcg_temp_new_i32();
|
||||
gen_aa32_ld32u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld32u_iss(s, tmp, addr, get_mem_index(s),
|
||||
rd | ISSIs16Bit);
|
||||
tcg_temp_free_i32(addr);
|
||||
store_reg(s, rd, tmp);
|
||||
break;
|
||||
|
@ -11117,28 +11205,28 @@ static void disas_thumb_insn(CPUARMState *env, DisasContext *s)
|
|||
|
||||
switch (op) {
|
||||
case 0: /* str */
|
||||
gen_aa32_st32(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st32_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
break;
|
||||
case 1: /* strh */
|
||||
gen_aa32_st16(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st16_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
break;
|
||||
case 2: /* strb */
|
||||
gen_aa32_st8(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st8_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
break;
|
||||
case 3: /* ldrsb */
|
||||
gen_aa32_ld8s(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld8s_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
break;
|
||||
case 4: /* ldr */
|
||||
gen_aa32_ld32u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld32u_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
break;
|
||||
case 5: /* ldrh */
|
||||
gen_aa32_ld16u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld16u_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
break;
|
||||
case 6: /* ldrb */
|
||||
gen_aa32_ld8u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld8u_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
break;
|
||||
case 7: /* ldrsh */
|
||||
gen_aa32_ld16s(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld16s_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
break;
|
||||
}
|
||||
if (op >= 3) { /* load */
|
||||
|
@ -11182,12 +11270,12 @@ static void disas_thumb_insn(CPUARMState *env, DisasContext *s)
|
|||
if (insn & (1 << 11)) {
|
||||
/* load */
|
||||
tmp = tcg_temp_new_i32();
|
||||
gen_aa32_ld8u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld8u_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
store_reg(s, rd, tmp);
|
||||
} else {
|
||||
/* store */
|
||||
tmp = load_reg(s, rd);
|
||||
gen_aa32_st8(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st8_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
tcg_temp_free_i32(tmp);
|
||||
}
|
||||
tcg_temp_free_i32(addr);
|
||||
|
@ -11204,12 +11292,12 @@ static void disas_thumb_insn(CPUARMState *env, DisasContext *s)
|
|||
if (insn & (1 << 11)) {
|
||||
/* load */
|
||||
tmp = tcg_temp_new_i32();
|
||||
gen_aa32_ld16u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld16u_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
store_reg(s, rd, tmp);
|
||||
} else {
|
||||
/* store */
|
||||
tmp = load_reg(s, rd);
|
||||
gen_aa32_st16(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st16_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
tcg_temp_free_i32(tmp);
|
||||
}
|
||||
tcg_temp_free_i32(addr);
|
||||
|
@ -11225,12 +11313,12 @@ static void disas_thumb_insn(CPUARMState *env, DisasContext *s)
|
|||
if (insn & (1 << 11)) {
|
||||
/* load */
|
||||
tmp = tcg_temp_new_i32();
|
||||
gen_aa32_ld32u(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_ld32u_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
store_reg(s, rd, tmp);
|
||||
} else {
|
||||
/* store */
|
||||
tmp = load_reg(s, rd);
|
||||
gen_aa32_st32(s, tmp, addr, get_mem_index(s));
|
||||
gen_aa32_st32_iss(s, tmp, addr, get_mem_index(s), rd | ISSIs16Bit);
|
||||
tcg_temp_free_i32(tmp);
|
||||
}
|
||||
tcg_temp_free_i32(addr);
|
||||
|
@ -11712,6 +11800,7 @@ void gen_intermediate_code(CPUARMState *env, TranslationBlock *tb)
|
|||
store_cpu_field(tmp, condexec_bits);
|
||||
}
|
||||
do {
|
||||
dc->insn_start_idx = tcg_op_buf_count();
|
||||
tcg_gen_insn_start(dc->pc,
|
||||
(dc->condexec_cond << 4) | (dc->condexec_mask >> 1),
|
||||
0);
|
||||
|
|
|
@ -104,6 +104,20 @@ static inline int default_exception_el(DisasContext *s)
|
|||
? 3 : MAX(1, s->current_el);
|
||||
}
|
||||
|
||||
static void disas_set_insn_syndrome(DisasContext *s, uint32_t syn)
|
||||
{
|
||||
/* We don't need to save all of the syndrome so we mask and shift
|
||||
* out unneeded bits to help the sleb128 encoder do a better job.
|
||||
*/
|
||||
syn &= ARM_INSN_START_WORD2_MASK;
|
||||
syn >>= ARM_INSN_START_WORD2_SHIFT;
|
||||
|
||||
/* We check and clear insn_start_idx to catch multiple updates. */
|
||||
assert(s->insn_start_idx != 0);
|
||||
tcg_set_insn_param(s->insn_start_idx, 2, syn);
|
||||
s->insn_start_idx = 0;
|
||||
}
|
||||
|
||||
/* target-specific extra values for is_jmp */
|
||||
/* These instructions trap after executing, so the A32/T32 decoder must
|
||||
* defer them until after the conditional execution state has been updated.
|
||||
|
|
Loading…
Reference in New Issue