android qemu-kvm i8254 pit虚拟设备
ubuntu12.04下使用android emulator,启用kvm加速,模拟i8254定时器的代码比较旧,对应于qemu0.14或者之前的版本,这时还没有QOM(qemu object model)模型,虚拟设备的代码是比较简单的。
玩虚拟设备之前,首先得搞明白真实设备怎么玩,有篇文档:http://blog.csdn.net/u013007900/article/details/50408903,看不太明白就再看看计组和哈工大出版的C语言测控,以前上课用的这个。
8254使用的端口时0x40~0x43,共计4个8bit端口,输入时钟频率1193kHZ,使用IRQ0,对应中断向量表中的INT 8。怎么对应,看http://www.360doc.com/content/09/1017/08/128139_7395798.shtml和http://blog.csdn.net/duguteng/article/details/7552774。
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
#include
#include
#include
#include /*参数*/
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); // 输入时钟频率1193kHZdisable();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_PITprintf("irq_level=%d next_delay=%f\n",irq_level,(double)(expire_time - current_time) / get_ticks_per_sec());
#endifs->next_transition_time = expire_time;if (expire_time != -1)timer_mod(s->irq_timer, expire_time);elsetimer_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);
}
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的性能还是有提升空间的。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
