android qemu-kvm i8254 pit虛擬裝置
ubuntu12.04下使用android emulator,啟用kvm加速,模擬i8254定時器的程式碼比較舊,對應於qemu0.14或者之前的版本,這時還沒有QOM(qemu object model)模型,虛擬裝置的程式碼是比較簡單的。
8259主片的IRQ0~7對應INT 8~INT F,從片的IRQ8~IRQ15對應INT 70~INT 77。
有份以前上C語言測控時寫的程式碼,使用了8254的,輸入取樣週期(in ms)和取樣次數,每次取樣時列印一個'8'。
注意定時器的最大週期比較短,大約55ms,所以需要使用軟體方式擴大定時器的週期,注意週期不是10ms的倍數時的特殊處理。
定時器0工作於模式3,方波發生器。用學硬體的話來說,就是自動重灌定時器;用學軟體的話來說,就是週期定時器,不是oneshot的。
/* C語言測控程式設計 * 2012年3月29日 * 系統XP sp3,編譯器:TC3.0,編輯器:VIM7.3 * */ #include <stdio.h> #include <dos.h> #include <graphics.h> #include <math.h> #include <string.h> /*引數*/ float gfT; //取樣週期 long glN; //取樣次數 int giFlag; //標記時間到 long glUserCnt; //已取樣次數 int giTimerN; //取樣週期除以10ms int giTimerSmallValue; //取樣週期模10ms後,對應的定時器初值 int giTimerCnt; //定時器中斷次數 void LoadConfig(void); //讀取配置檔案 void interrupt (*OldIsr08)(void); //原先的中斷函式指標 void interrupt MyIsr08(void); //自定義的中斷函式 void TimerInit(void); //定時器初始化函式 void TimerExit(void); //定時器恢復函式 void UserTimerIsr(void); //每個取樣週期都會呼叫的函式 int main() { /*讀取配置*/ LoadConfig(); /*初始化*/ TimerInit(); while((glUserCnt < glN) || (glN == 0)) { if(kbhit()) //特定按鍵退出 { if(getch() == ' ') break; } if(giFlag) { giFlag = 0; putchar('8'); } } /*恢復定時器和dos介面*/ TimerExit(); printf("\nthe times of interrupt is: %ld\n",glUserCnt); getch(); return 0; } /*定時器中斷函式,每到使用者設定的時間,呼叫一次UserTimerIsr()*/ void interrupt MyIsr08(void) { giTimerCnt++; if(giTimerN == 0) //取樣週期小於10ms的情況 { giTimerCnt = 0; UserTimerIsr(); outportb(0x20, 0x20); //清除中斷標誌位,可以看8259相關的資料 return; } if((giTimerSmallValue == 0) && (giTimerCnt == giTimerN)) //取樣週期是10ms的倍數的情況 { giTimerCnt = 0; UserTimerIsr(); outportb(0x20, 0x20); return; } if((giTimerSmallValue != 0) && (giTimerN != 0)) //取樣週期大於10ms,且不是10ms倍數的情況 { if(giTimerCnt == 1) { disable(); outportb(0x43, 0x36); outportb(0x40, 0x9d); outportb(0x40, 0x2e); enable(); } if(giTimerCnt == (giTimerN + 1)) { giTimerCnt = 0; disable(); outportb(0x43, 0x36); outportb(0x40, giTimerSmallValue & 0xff); outportb(0x40, (giTimerSmallValue >> 8) & 0xff); enable(); UserTimerIsr(); } outportb(0x20, 0x20); return; } outportb(0x20, 0x20); } /*初始化定時器*/ void TimerInit(void) { giTimerN = (int)(gfT / 10); giTimerSmallValue = (int)((gfT - giTimerN * 10) * 1193); // 輸入時鐘頻率1193kHZ disable(); OldIsr08 = getvect(0x08); if(giTimerSmallValue) { outportb(0x43, 0x36); outportb(0x40, giTimerSmallValue & 0xff); outportb(0x40, (giTimerSmallValue >> 8) & 0xff); } else { outportb(0x43, 0x36); outportb(0x40, 0x9d); outportb(0x40, 0x2e); } setvect(0x08, MyIsr08); enable(); } /*恢復定時器原先的服務函式和週期*/ void TimerExit(void) { disable(); outportb(0x43, 0x36); outportb(0x40, 0x00); outportb(0x40, 0x00); setvect(0x08, OldIsr08); enable(); } /*每個取樣週期都會呼叫的函式*/ void UserTimerIsr(void) { glUserCnt++; giFlag = 1; } /*獲取配置資訊*/ void LoadConfig(void) { printf("input T and N\n"); scanf("%f %ld", &gfT, &glN); while(getchar() != 10); if( gfT <= 0 || glN < 0) { printf("error, try again\n"); LoadConfig(); } }
真的看完了,現在開始看模擬的。
8254的初始化是在pc_init1中執行的,設定iobase為0x40,IRQ為0,INT 8:
pit = pit_init(0x40, i8259[0]);
8254是有三個timer的,只用到了channel 0的timer。
qemu有自己的定時器,輸入時鐘是1G,對應1ns。8254的輸入時鐘是1193kHZ,如何模擬的呢?
根據8254的設定,計算出來下一個中斷到臨的tick次數,在根據8254和qemu timer頻率的不同,對tick進行轉換,然後設定qemu timer的定時設定,當qemu timer超時時,callback函式就是8254的中斷處理函式pit_irq_timer。在中斷函式中,再進行一些其它的處理,如重新裝載之類的。
PITState *pit_init(int base, qemu_irq irq)
{
PITState *pit = &pit_state;
PITChannelState *s;
s = &pit->channels[0];
/* the timer 0 is connected to an IRQ */
s->irq_timer = timer_new(QEMU_CLOCK_VIRTUAL, SCALE_NS, pit_irq_timer, s);
s->irq = irq;
register_savevm(NULL, "i8254", base, 1, pit_save, pit_load, pit);
qemu_register_reset(pit_reset, 0, pit);
register_ioport_write(base, 4, 1, pit_ioport_write, pit);
register_ioport_read(base, 3, 1, pit_ioport_read, pit);
pit_reset(pit);
return pit;
}
qemu_register_reset是用連結串列儲存一些復位函式的:
void qemu_register_reset(QEMUResetHandler *func, int order, void *opaque)
{
QEMUResetEntry **pre, *re;
pre = &first_reset_entry;
while (*pre != NULL && (*pre)->order >= order) {
pre = &(*pre)->next;
}
re = g_malloc0(sizeof(QEMUResetEntry));
re->func = func;
re->opaque = opaque;
re->order = order;
re->next = NULL;
*pre = re;
}
當然pit_init最後也呼叫了pit_reset函式對暫存器進行復位,將mode設定為3,設定gate,計數值歸零:
static void pit_reset(void *opaque)
{
PITState *pit = opaque;
PITChannelState *s;
int i;
for(i = 0;i < 3; i++) {
s = &pit->channels[i];
s->mode = 3;
s->gate = (i != 2);
pit_load_count(s, 0);
}
}
這兩行設定了暫存器的讀寫函式,注意這裡是PMIO方式,不是MMIO方式的暫存器。0x40~0x43的寫函式設定為pit_ioport_write;0x40~0x42的讀函式設定為pit_ioport_read:
register_ioport_write(base, 4, 1, pit_ioport_write, pit);
register_ioport_read(base, 3, 1, pit_ioport_read, pit);
寫函式,看懂暫存器的使用後,這個函式還是比較簡單的:
static void pit_ioport_write(void *opaque, uint32_t addr, uint32_t val)
{
PITState *pit = opaque;
int channel, access;
PITChannelState *s;
addr &= 3;
if (addr == 3) {
channel = val >> 6;
if (channel == 3) {
/* read back command */
for(channel = 0; channel < 3; channel++) {
s = &pit->channels[channel];
if (val & (2 << channel)) {
if (!(val & 0x20)) {
pit_latch_count(s);
}
if (!(val & 0x10) && !s->status_latched) {
/* status latch */
/* XXX: add BCD and null count */
s->status = (pit_get_out1(s, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) 7) |
(s->rw_mode << 4) |
(s->mode << 1) |
s->bcd;
s->status_latched = 1;
}
}
}
} else {
s = &pit->channels[channel];
access = (val >> 4) & 3;
if (access == 0) {
pit_latch_count(s);
} else {
s->rw_mode = access;
s->read_state = access;
s->write_state = access;
s->mode = (val >> 1) & 7;
s->bcd = val & 1;
/* XXX: update irq timer ? */
}
}
} else {
s = &pit->channels[addr];
switch(s->write_state) {
default:
case RW_STATE_LSB:
pit_load_count(s, val);
break;
case RW_STATE_MSB:
pit_load_count(s, val << 8);
break;
case RW_STATE_WORD0:
s->write_latch = val;
s->write_state = RW_STATE_WORD1;
break;
case RW_STATE_WORD1:
pit_load_count(s, s->write_latch | (val << 8));
s->write_state = RW_STATE_WORD0;
break;
}
}
}
pit_latch_count用於鎖存當前的計數值:
static void pit_latch_count(PITChannelState *s)
{
if (!s->count_latched) {
s->latched_count = pit_get_count(s);
s->count_latched = s->rw_mode;
}
}
pit_load_count用於裝載計數值,count_load_time是裝載時tick的值(tick++ in every ns);count是8254的週期,8254自己的計數值會按照1193kHZ的頻率遞減的。注意和count_load_time單位的不同,以及後續單位的轉換。最後呼叫pit_irq_timer_update,對qemu timer進行更新。
static inline void pit_load_count(PITChannelState *s, int val)
{
if (val == 0)
val = 0x10000;
s->count_load_time = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
s->count = val;
pit_irq_timer_update(s, s->count_load_time);
}
pit_irq_timer_update函式幹兩件事:
1、計算irq_level,就是比較tick的值和設定的值,滿足條件時就會qemu_set_irq觸發中斷請求
2、計算expire_time,並且呼叫timer_mod更新qemu timer,讓qemu timer在8254下一個需要產生中斷的時候產生timeout,並呼叫callback,也就是8254的中斷函式
static void pit_irq_timer_update(PITChannelState *s, int64_t current_time)
{
int64_t expire_time;
int irq_level;
if (!s->irq_timer)
return;
expire_time = pit_get_next_transition_time(s, current_time);
irq_level = pit_get_out1(s, current_time);
qemu_set_irq(s->irq, irq_level);
#ifdef DEBUG_PIT
printf("irq_level=%d next_delay=%f\n",
irq_level,
(double)(expire_time - current_time) / get_ticks_per_sec());
#endif
s->next_transition_time = expire_time;
if (expire_time != -1)
timer_mod(s->irq_timer, expire_time);
else
timer_del(s->irq_timer);
}
8254的中斷函式,也就是qemu timer的callback函式,也呼叫了pit_irq_timer_update:
static void pit_irq_timer(void *opaque)
{
PITChannelState *s = opaque;
pit_irq_timer_update(s, s->next_transition_time);
}
暫存器的讀函式:
static uint32_t pit_ioport_read(void *opaque, uint32_t addr)
{
PITState *pit = opaque;
int ret, count;
PITChannelState *s;
addr &= 3;
s = &pit->channels[addr];
if (s->status_latched) {
s->status_latched = 0;
ret = s->status;
} else if (s->count_latched) {
switch(s->count_latched) {
default:
case RW_STATE_LSB:
ret = s->latched_count & 0xff;
s->count_latched = 0;
break;
case RW_STATE_MSB:
ret = s->latched_count >> 8;
s->count_latched = 0;
break;
case RW_STATE_WORD0:
ret = s->latched_count & 0xff;
s->count_latched = RW_STATE_MSB;
break;
}
} else {
switch(s->read_state) {
default:
case RW_STATE_LSB:
count = pit_get_count(s);
ret = count & 0xff;
break;
case RW_STATE_MSB:
count = pit_get_count(s);
ret = (count >> 8) & 0xff;
break;
case RW_STATE_WORD0:
count = pit_get_count(s);
ret = count & 0xff;
s->read_state = RW_STATE_WORD1;
break;
case RW_STATE_WORD1:
count = pit_get_count(s);
ret = (count >> 8) & 0xff;
s->read_state = RW_STATE_WORD0;
break;
}
}
return ret;
}
當kvm執行到PMIO的操作時,會退出,然後呼叫kvm_handle_io:
case KVM_EXIT_IO:
dprintf("handle_io\n");
ret = kvm_handle_io(cpu, run->io.port,
(uint8_t *)run + run->io.data_offset,
run->io.direction,
run->io.size,
run->io.count);
break;
static int kvm_handle_io(CPUState *cpu, uint16_t port, void *data,
int direction, int size, uint32_t count)
{
int i;
uint8_t *ptr = data;
for (i = 0; i < count; i++) {
if (direction == KVM_EXIT_IO_IN) {
switch (size) {
case 1:
stb_p(ptr, cpu_inb(port));
break;
case 2:
stw_p(ptr, cpu_inw(port));
break;
case 4:
stl_p(ptr, cpu_inl(port));
break;
}
} else {
switch (size) {
case 1:
cpu_outb(port, ldub_p(ptr));
break;
case 2:
cpu_outw(port, lduw_p(ptr));
break;
case 4:
cpu_outl(port, ldl_p(ptr));
break;
}
}
ptr += size;
}
return 1;
}
以8bit讀為例子:
uint8_t cpu_inb(pio_addr_t addr)
{
uint8_t val;
val = ioport_read(0, addr);
LOG_IOPORT("inb : %04"FMT_pioaddr" %02"PRIx8"\n", addr, val);
return val;
}
static uint32_t ioport_read(int index, uint32_t address)
{
static IOPortReadFunc * const default_func[3] = {
default_ioport_readb,
default_ioport_readw,
default_ioport_readl
};
IOPortReadFunc *func = ioport_read_table[index][address];
if (!func)
func = default_func[index];
return func(ioport_opaque[address], address);
}
PMIO的地址和opaque以及讀寫函式的繫結,使用register_ioport_read,register_ioport_write函式,在i8254.c的pit_init中呼叫的:int register_ioport_read(pio_addr_t start, int length, int size,
IOPortReadFunc *func, void *opaque)
{
pio_addr_t i;
int bsize;
if (ioport_bsize(size, &bsize)) {
hw_error("register_ioport_read: invalid size");
return -1;
}
for(i = start; i < start + length; i += size) {
ioport_read_table[bsize][i] = func;
if (ioport_opaque[i] != NULL && ioport_opaque[i] != opaque)
hw_error("register_ioport_read: invalid opaque");
ioport_opaque[i] = opaque;
}
return 0;
}
pit_save,pit_load,register_savevm用於快照和恢復的,可以不看。
現在qemu的8254都是使用了QOM模型了,這個模型太TMD的複雜了。另外hw/i386/kvm/timer/i8254.c中提供了kvm-pit,使用kvm提供的核心態的8254的模擬,中斷的處理和IO的讀寫都在核心態,不需要退出kvm了,速度要更快些。類似的,8259之類的也有kvm核心態的實現,所以說android emulator的效能還是有提升空間的。