Model the A6 crypto, interrupt, USB, and platform blocks needed to boot SecureROM through iBSS into iBEC Recovery. Add local lab identity, IMG3, and APTicket tooling, patched macOS recovery utilities, UART and GDB access, and English end-user documentation.
385 lines
11 KiB
C
385 lines
11 KiB
C
/*
|
|
* Apple Interrupt Controller v2 as used by the S5L8950X.
|
|
*
|
|
* The A6 SecureROM uses the same register layout later documented for AICv2:
|
|
* event acknowledge at 0x2004, per-source destinations at 0x3000 and mask
|
|
* set/clear banks at 0x4100/0x4180.
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "qemu/log.h"
|
|
#include "qemu/timer.h"
|
|
#include "hw/core/cpu.h"
|
|
#include "hw/core/sysbus.h"
|
|
#include "hw/core/irq.h"
|
|
#include "qom/object.h"
|
|
#include "target/arm/cpu.h"
|
|
|
|
#define TYPE_S5L8950X_AIC "s5l8950x-aic"
|
|
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XAicState, S5L8950X_AIC)
|
|
|
|
#define S5L8950X_AIC_NUM_IRQS 256
|
|
#define S5L8950X_AIC_NUM_BANKS (S5L8950X_AIC_NUM_IRQS / 32)
|
|
|
|
#define AIC_REV 0x0000
|
|
#define AIC_CAP0 0x0004
|
|
#define AIC_CAP1 0x0008
|
|
#define AIC_RST 0x000c
|
|
#define AIC_GLB_CFG 0x0010
|
|
#define AIC_TIME_LO 0x0020
|
|
#define AIC_TIME_HI 0x0028
|
|
#define AIC_WHOAMI 0x2000
|
|
#define AIC_ACK 0x2004
|
|
#define AIC_IPI_SET 0x2008
|
|
#define AIC_IPI_CLR 0x200c
|
|
#define AIC_TIMER_CTRL 0x2010
|
|
#define AIC_TIMER_COMPARE 0x2014
|
|
#define AIC_TIMER_STATE 0x2018
|
|
#define AIC_TIMER_MASK_SET 0x201c
|
|
#define AIC_TIMER_MASK_CLR 0x2020
|
|
#define AIC_SRC_CFG_BASE 0x3000
|
|
#define AIC_MASK_SET_BASE 0x4100
|
|
#define AIC_MASK_CLR_BASE 0x4180
|
|
#define AIC_INT_STATE_BASE 0x4200
|
|
|
|
#define AIC_EVENT_EXT 0x00010000
|
|
#define AIC_EVENT_TIMER 0x00070001
|
|
#define AIC_CLOCK_HZ 24000000
|
|
|
|
struct S5L8950XAicState {
|
|
SysBusDevice parent_obj;
|
|
MemoryRegion iomem;
|
|
uint32_t mask[S5L8950X_AIC_NUM_BANKS];
|
|
uint32_t pending[S5L8950X_AIC_NUM_BANKS];
|
|
uint32_t src_cfg[S5L8950X_AIC_NUM_IRQS];
|
|
uint32_t glb_cfg;
|
|
uint32_t timer_ctrl;
|
|
uint32_t timer_compare;
|
|
uint32_t timer_state;
|
|
bool timer_irq_enabled;
|
|
bool timer_pending;
|
|
bool timer_has_fired;
|
|
QEMUTimer *timer;
|
|
qemu_irq irq;
|
|
qemu_irq fiq;
|
|
};
|
|
|
|
static bool s5l8950x_aic_irq_is_active(S5L8950XAicState *s, int irq)
|
|
{
|
|
int bank = irq / 32;
|
|
uint32_t bit = BIT(irq % 32);
|
|
|
|
return (s->pending[bank] & bit) && !(s->mask[bank] & bit) &&
|
|
(s->src_cfg[irq] & 1);
|
|
}
|
|
|
|
static void s5l8950x_aic_update(S5L8950XAicState *s)
|
|
{
|
|
bool active = s->timer_pending && s->timer_irq_enabled;
|
|
int irq;
|
|
|
|
for (irq = 0; irq < S5L8950X_AIC_NUM_IRQS; irq++) {
|
|
if (s5l8950x_aic_irq_is_active(s, irq)) {
|
|
active = true;
|
|
break;
|
|
}
|
|
}
|
|
qemu_set_irq(s->irq, active);
|
|
qemu_irq_lower(s->fiq);
|
|
}
|
|
|
|
static uint64_t s5l8950x_aic_ticks(void)
|
|
{
|
|
return muldiv64(qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL), AIC_CLOCK_HZ,
|
|
NANOSECONDS_PER_SECOND);
|
|
}
|
|
|
|
static void s5l8950x_aic_timer_reschedule(S5L8950XAicState *s);
|
|
|
|
static void s5l8950x_aic_timer_expired(void *opaque)
|
|
{
|
|
S5L8950XAicState *s = opaque;
|
|
CPUState *cs = first_cpu;
|
|
vaddr pc = cs ? ARM_CPU(cs)->env.regs[15] : 0;
|
|
|
|
/*
|
|
* iBoot initially shares the top of its bootstrap stack with the IRQ
|
|
* stack. On A6 the CPU reaches WFE well before the first 10 ms timer
|
|
* deadline, while a non-icount TCG run can take longer in host time.
|
|
* Do not inject that first local event until the bootstrap code has
|
|
* actually reached WFE; later events retain their normal timing and may
|
|
* preempt scheduler tasks.
|
|
*/
|
|
if (!s->timer_has_fired && cs &&
|
|
(!cs->halted || pc < 0x10000854 || pc > 0x10000860)) {
|
|
timer_mod(s->timer,
|
|
qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
|
|
NANOSECONDS_PER_SECOND / 1000);
|
|
return;
|
|
}
|
|
|
|
if (!s->timer_has_fired) {
|
|
qemu_log_mask(LOG_GUEST_ERROR,
|
|
"s5l8950x-aic: first local timer event at "
|
|
"PC=0x%" VADDR_PRIx "\n", pc);
|
|
}
|
|
s->timer_has_fired = true;
|
|
s->timer_state |= 1;
|
|
s->timer_pending = true;
|
|
s5l8950x_aic_update(s);
|
|
}
|
|
|
|
static void s5l8950x_aic_timer_reschedule(S5L8950XAicState *s)
|
|
{
|
|
uint32_t delta = s->timer_compare;
|
|
int64_t delta_ns;
|
|
|
|
timer_del(s->timer);
|
|
/* AICv2's compare register is a relative countdown in 24 MHz ticks. */
|
|
if (!(s->timer_ctrl & 1) || delta == UINT32_MAX) {
|
|
return;
|
|
}
|
|
if (!delta) {
|
|
delta = 1;
|
|
}
|
|
delta_ns = muldiv64(delta, NANOSECONDS_PER_SECOND, AIC_CLOCK_HZ);
|
|
if (!delta_ns) {
|
|
delta_ns = 1;
|
|
}
|
|
timer_mod(s->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + delta_ns);
|
|
}
|
|
|
|
static uint32_t s5l8950x_aic_ack(S5L8950XAicState *s)
|
|
{
|
|
int irq;
|
|
|
|
/*
|
|
* The per-CPU timer is reported by AICv2 as local event 1, not as an
|
|
* external source. iBoot maps 0x00070001 to its internal vector 0xc1.
|
|
* Acknowledge also masks it until iBoot writes TIMER_MASK_CLR at the end
|
|
* of the handler, mirroring the external-source acknowledge behaviour.
|
|
*/
|
|
if (s->timer_pending && s->timer_irq_enabled) {
|
|
s->timer_irq_enabled = false;
|
|
s5l8950x_aic_update(s);
|
|
return AIC_EVENT_TIMER;
|
|
}
|
|
|
|
for (irq = 0; irq < S5L8950X_AIC_NUM_IRQS; irq++) {
|
|
if (s5l8950x_aic_irq_is_active(s, irq)) {
|
|
s->mask[irq / 32] |= BIT(irq % 32);
|
|
s5l8950x_aic_update(s);
|
|
return AIC_EVENT_EXT | irq;
|
|
}
|
|
}
|
|
s5l8950x_aic_update(s);
|
|
return 0;
|
|
}
|
|
|
|
static void s5l8950x_aic_set_irq(void *opaque, int irq, int level)
|
|
{
|
|
S5L8950XAicState *s = opaque;
|
|
uint32_t bit = BIT(irq % 32);
|
|
|
|
if (level) {
|
|
s->pending[irq / 32] |= bit;
|
|
} else {
|
|
s->pending[irq / 32] &= ~bit;
|
|
}
|
|
s5l8950x_aic_update(s);
|
|
}
|
|
|
|
static uint64_t s5l8950x_aic_read(void *opaque, hwaddr offset, unsigned size)
|
|
{
|
|
S5L8950XAicState *s = opaque;
|
|
uint64_t ticks;
|
|
|
|
ticks = s5l8950x_aic_ticks();
|
|
switch (offset) {
|
|
case AIC_REV:
|
|
return 2;
|
|
case AIC_CAP0:
|
|
return S5L8950X_AIC_NUM_IRQS;
|
|
case AIC_CAP1:
|
|
return 0;
|
|
case AIC_RST:
|
|
return 0;
|
|
case AIC_GLB_CFG:
|
|
return s->glb_cfg;
|
|
case AIC_TIME_LO:
|
|
return (uint32_t)ticks;
|
|
case AIC_TIME_HI:
|
|
return (uint32_t)(ticks >> 32);
|
|
case AIC_WHOAMI:
|
|
return 0;
|
|
case AIC_ACK:
|
|
return s5l8950x_aic_ack(s);
|
|
case AIC_TIMER_CTRL:
|
|
return s->timer_ctrl;
|
|
case AIC_TIMER_COMPARE:
|
|
return s->timer_compare;
|
|
case AIC_TIMER_STATE:
|
|
return s->timer_state;
|
|
case AIC_TIMER_MASK_SET:
|
|
case AIC_TIMER_MASK_CLR:
|
|
return 0;
|
|
case AIC_SRC_CFG_BASE ... AIC_SRC_CFG_BASE +
|
|
S5L8950X_AIC_NUM_IRQS * 4 - 4:
|
|
return s->src_cfg[(offset - AIC_SRC_CFG_BASE) / 4];
|
|
case AIC_MASK_SET_BASE ... AIC_MASK_SET_BASE +
|
|
S5L8950X_AIC_NUM_BANKS * 4 - 4:
|
|
return s->mask[(offset - AIC_MASK_SET_BASE) / 4];
|
|
case AIC_MASK_CLR_BASE ... AIC_MASK_CLR_BASE +
|
|
S5L8950X_AIC_NUM_BANKS * 4 - 4:
|
|
return s->mask[(offset - AIC_MASK_CLR_BASE) / 4];
|
|
case AIC_INT_STATE_BASE ... AIC_INT_STATE_BASE +
|
|
S5L8950X_AIC_NUM_BANKS * 4 - 4:
|
|
return s->pending[(offset - AIC_INT_STATE_BASE) / 4];
|
|
default:
|
|
qemu_log_mask(LOG_UNIMP,
|
|
"%s: unimplemented read at 0x%" HWADDR_PRIx "\n",
|
|
__func__, offset);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static void s5l8950x_aic_write(void *opaque, hwaddr offset,
|
|
uint64_t value, unsigned size)
|
|
{
|
|
S5L8950XAicState *s = opaque;
|
|
|
|
switch (offset) {
|
|
case AIC_RST:
|
|
memset(s->mask, 0xff, sizeof(s->mask));
|
|
memset(s->pending, 0, sizeof(s->pending));
|
|
s5l8950x_aic_update(s);
|
|
break;
|
|
case AIC_GLB_CFG:
|
|
s->glb_cfg = value;
|
|
break;
|
|
case AIC_IPI_SET:
|
|
case AIC_IPI_CLR:
|
|
break;
|
|
case AIC_TIMER_CTRL:
|
|
s->timer_ctrl = value;
|
|
if (!(s->timer_ctrl & 1)) {
|
|
timer_del(s->timer);
|
|
}
|
|
break;
|
|
case AIC_TIMER_COMPARE:
|
|
s->timer_compare = value;
|
|
s5l8950x_aic_timer_reschedule(s);
|
|
break;
|
|
case AIC_TIMER_STATE:
|
|
s->timer_state &= ~value;
|
|
if (value & 1) {
|
|
s->timer_pending = false;
|
|
s5l8950x_aic_update(s);
|
|
}
|
|
break;
|
|
case AIC_TIMER_MASK_SET:
|
|
if (value & 2) {
|
|
s->timer_irq_enabled = false;
|
|
s5l8950x_aic_update(s);
|
|
}
|
|
break;
|
|
case AIC_TIMER_MASK_CLR:
|
|
if (value & 2) {
|
|
s->timer_irq_enabled = true;
|
|
s5l8950x_aic_update(s);
|
|
}
|
|
break;
|
|
case AIC_SRC_CFG_BASE ... AIC_SRC_CFG_BASE +
|
|
S5L8950X_AIC_NUM_IRQS * 4 - 4:
|
|
s->src_cfg[(offset - AIC_SRC_CFG_BASE) / 4] = value;
|
|
s5l8950x_aic_update(s);
|
|
break;
|
|
case AIC_MASK_SET_BASE ... AIC_MASK_SET_BASE +
|
|
S5L8950X_AIC_NUM_BANKS * 4 - 4:
|
|
s->mask[(offset - AIC_MASK_SET_BASE) / 4] |= value;
|
|
s5l8950x_aic_update(s);
|
|
break;
|
|
case AIC_MASK_CLR_BASE ... AIC_MASK_CLR_BASE +
|
|
S5L8950X_AIC_NUM_BANKS * 4 - 4:
|
|
s->mask[(offset - AIC_MASK_CLR_BASE) / 4] &= ~value;
|
|
s5l8950x_aic_update(s);
|
|
break;
|
|
default:
|
|
qemu_log_mask(LOG_UNIMP,
|
|
"%s: unimplemented write at 0x%" HWADDR_PRIx
|
|
" = 0x%08" PRIx64 "\n",
|
|
__func__, offset, value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static const MemoryRegionOps s5l8950x_aic_ops = {
|
|
.read = s5l8950x_aic_read,
|
|
.write = s5l8950x_aic_write,
|
|
.endianness = DEVICE_LITTLE_ENDIAN,
|
|
.valid = {
|
|
.min_access_size = 4,
|
|
.max_access_size = 4,
|
|
},
|
|
.impl = {
|
|
.min_access_size = 4,
|
|
.max_access_size = 4,
|
|
},
|
|
};
|
|
|
|
static void s5l8950x_aic_reset(DeviceState *dev)
|
|
{
|
|
S5L8950XAicState *s = S5L8950X_AIC(dev);
|
|
|
|
memset(s->mask, 0xff, sizeof(s->mask));
|
|
memset(s->pending, 0, sizeof(s->pending));
|
|
memset(s->src_cfg, 0, sizeof(s->src_cfg));
|
|
s->glb_cfg = 0;
|
|
s->timer_ctrl = 0;
|
|
s->timer_compare = UINT32_MAX;
|
|
s->timer_state = 0;
|
|
s->timer_irq_enabled = false;
|
|
s->timer_pending = false;
|
|
s->timer_has_fired = false;
|
|
timer_del(s->timer);
|
|
s5l8950x_aic_update(s);
|
|
}
|
|
|
|
static void s5l8950x_aic_init(Object *obj)
|
|
{
|
|
S5L8950XAicState *s = S5L8950X_AIC(obj);
|
|
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
|
|
|
s->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
|
|
s5l8950x_aic_timer_expired, s);
|
|
memory_region_init_io(&s->iomem, obj, &s5l8950x_aic_ops, s,
|
|
TYPE_S5L8950X_AIC, 0x8000);
|
|
sysbus_init_mmio(sbd, &s->iomem);
|
|
sysbus_init_irq(sbd, &s->irq);
|
|
sysbus_init_irq(sbd, &s->fiq);
|
|
qdev_init_gpio_in(DEVICE(obj), s5l8950x_aic_set_irq,
|
|
S5L8950X_AIC_NUM_IRQS);
|
|
}
|
|
|
|
static void s5l8950x_aic_class_init(ObjectClass *klass, const void *data)
|
|
{
|
|
DeviceClass *dc = DEVICE_CLASS(klass);
|
|
|
|
device_class_set_legacy_reset(dc, s5l8950x_aic_reset);
|
|
}
|
|
|
|
static const TypeInfo s5l8950x_aic_info = {
|
|
.name = TYPE_S5L8950X_AIC,
|
|
.parent = TYPE_SYS_BUS_DEVICE,
|
|
.instance_size = sizeof(S5L8950XAicState),
|
|
.instance_init = s5l8950x_aic_init,
|
|
.class_init = s5l8950x_aic_class_init,
|
|
};
|
|
|
|
static void s5l8950x_aic_register_types(void)
|
|
{
|
|
type_register_static(&s5l8950x_aic_info);
|
|
}
|
|
|
|
type_init(s5l8950x_aic_register_types)
|