文章首发于

https://forum.butian.net/share/124

概述

上一篇文件介绍了luaqemu的实现,也提到luaqemu并没有对中断相关api进行封装,本节主要基于stm32f205-soc的实现来介绍中断的仿真,并提供一个用于测试qemu设备模拟的裸板程序来测试中断的仿真。

本文相关代码地址

https://github.com/hac425xxx/qemu-fuzzing/commit/609538e1407de884f6c9e4d222431c9032abc25b
https://github.com/hac425xxx/qemu-fuzzing/commit/7bc0e0aa35363c18fcf5b89dacab73a0a9bef147

stm32f205-soc实现

为了仿真某个设备,我们需要通过阅读硬件文档或者通过逆向程序逻辑来获取外设的行为,然后再在qemu中进行模拟,stm32f205的手册可以直接在网上下载

https://www.st.com/resource/en/reference_manual/cd00225773-stm32f205xx-stm32f207xx-stm32f215xx-and-stm32f217xx-advanced-arm-b ased-32-bit-mcus-stmicroelectronics.pdf

qemu中名字为netduino2的Machine使用到了stm32f205-soc这个设备,可以使用 -M 指定使用该设备

qemu-system-arm -M netduino2

netduino2的初始化函数为netduino2_init

static void netduino2_init(MachineState *machine)
{
DeviceState *dev; dev = qdev_create(NULL, TYPE_STM32F205_SOC);
qdev_prop_set_string(dev, "cpu-type", ARM_CPU_TYPE_NAME("cortex-m3"));
o bject_property_set_bool(o bject(dev), true, "realized", &error_fatal); armv7m_load_kernel(ARM_CPU(first_cpu), machine->kernel_filename,
FLASH_SIZE);
}

函数逻辑如下:

  1. 首先创建stm32f205-soc设备,然后设置cpu-type为 cortex-m3
  2. 然后通过设置 realized 触发stm32f205_soc_realize函数的调用
  3. 最后armv7m_load_kernel把命令行-kernel指定的文件加载到虚拟机内存。
static void stm32f205_soc_class_init(o bjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass); dc->realize = stm32f205_soc_realize;
dc->props = stm32f205_soc_properties;
} static const TypeInfo stm32f205_soc_info = {
.name = TYPE_STM32F205_SOC,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(STM32F205State),
.instance_init = stm32f205_soc_initfn,
.class_init = stm32f205_soc_class_init,
};

下面分析stm32f205_soc_realize的实现

初始化flash和sram

stm32f205的内存映射如下

stm32f205_soc_realize主要实现了红框标注的三个内存区域

  1. 位于0x8000000处的flash区域
  2. 位于0x0处的区域,是flash的alias区域
  3. 位于0x20000000处的sram区域

函数入口首先设置了flash和sram.

    MemoryRegion *system_memory = get_system_memory();
MemoryRegion *sram = g_new(MemoryRegion, 1);
MemoryRegion *flash = g_new(MemoryRegion, 1);
MemoryRegion *flash_alias = g_new(MemoryRegion, 1); MemoryRegion *demo_mem = g_new(MemoryRegion, 1); memory_region_init_ram(flash, NULL, "STM32F205.flash", FLASH_SIZE,
&error_fatal);
memory_region_init_alias(flash_alias, NULL, "STM32F205.flash.alias",
flash, 0, FLASH_SIZE); memory_region_set_readonly(flash, true);
memory_region_set_readonly(flash_alias, true); memory_region_add_subregion(system_memory, FLASH_b ase_ADDRESS, flash);
memory_region_add_subregion(system_memory, 0, flash_alias); memory_region_init_ram(sram, NULL, "STM32F205.sram", SRAM_SIZE,
&error_fatal);
memory_region_add_subregion(system_memory, SRAM_b ase_ADDRESS, sram);
  1. 主要就是新建flash区域和flash_alias,然后通过memory_region_add_subregion把这两个区域放到对应的地址,这样0x0和0x8000000实际指向的是同一块RAM。
  2. 然后新建sram区域,并把sram放到0x20000000处。

初始化外设

在初始化flash和sram后,会逐步初始化用到的外设,这里以UART外设为例进行介绍

UART外设

初始化

uart使用sysbus_mmio_map把外设的寄存器区域映射为mmio内存,然后使用sysbus_connect_irq初始化外设需要的irq。

    /* Attach UART (uses USART registers) and USART controllers */
for (i = 0; i < STM_NUM_USARTS; i++) {
dev = DEVICE(&(s->usart[i]));
qdev_prop_set_chr(dev, "chardev", serial_hd(i));
o bject_property_set_bool(o bject(&s->usart[i]), true, "realized", &err);
if (err != NULL) {
error_propagate(errp, err);
return;
}
busdev = SYS_BUS_DEVICE(dev);
sysbus_mmio_map(busdev, 0, usart_addr[i]);
sysbus_connect_irq(busdev, 0, qdev_get_gpio_in(armv7m, usart_irq[i]));
}

s->usart在stm32f205_soc_initfn中创建

static void stm32f205_soc_initfn(o bject *obj)
{
for (i = 0; i < STM_NUM_USARTS; i++) {
sysbus_init_child_obj(obj, "usart[*]", &s->usart[i],
sizeof(s->usart[i]), TYPE_STM32F2XX_USART);
}

实际就是创建了TYPE_STM32F2XX_USART设备

static const TypeInfo stm32f2xx_usart_info = {
.name = TYPE_STM32F2XX_USART,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(STM32F2XXUsartState),
.instance_init = stm32f2xx_usart_init,
.class_init = stm32f2xx_usart_class_init,
};

调用sysbus_init_child_obj函数初始化设备时会调用stm32f2xx_usart_init

static const MemoryRegionOps stm32f2xx_usart_ops = {
.read = stm32f2xx_usart_read,
.write = stm32f2xx_usart_write,
.endianness = DEVICE_NATIVE_ENDIAN,
}; static void stm32f2xx_usart_init(o bject *obj)
{
STM32F2XXUsartState *s = STM32F2XX_USART(obj); sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq); memory_region_init_io(&s->mmio, obj, &stm32f2xx_usart_ops, s,
TYPE_STM32F2XX_USART, 0x400);
sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
}

函数做的工作如下

  1. 初始化设备的irq,保存到s->irq
  2. 初始化s->mmio,设置memory_region的大小为0x400,mmio内存访问的回调函数由stm32f2xx_usart_ops指定
  3. sysbus_init_mmio主要是把s->mmio的指针保存到设备mmio数组中,以便后续使用sysbus_mmio_map把memory_region挂载到对应的地址。

mmio映射

stm32f205-soc实现了6个uart设备,设备mmio的起始地址分别为

static const uint32_t usart_addr[STM_NUM_USARTS] = { 0x40011000, 0x40004400,
0x40004800, 0x40004C00, 0x40005000, 0x40011400 };

其中4个uart设备在手册memory map中的截图如下

然后在stm32f205_soc_realize函数里面会调用sysbus_mmio_map把设备的memory_region挂载到指定的位置

sysbus_mmio_map(busdev, 0, usart_addr[i]);

中断初始化

qemu中断模型

概念

qemu使用GPIO来实现中断系统,其简单的原理如下

Device.[GPIO_OUT] ->[GPIO_IN].GIC.[GPIO_OUT]->[GPIO_IN].core
  1. 首先CPU有GPIO_IN接口
  2. 然后中断控制器(GIC)有GPIO_IN和GPIO_OUT, GPIO_OUT和CPU的GPIO_IN接口关联
  3. 设备的GPIO_OUT和GIC的GPIO_IN关联
  4. 当有中断发生时,设备通过GPIO_OUT通知GIC,GIC通过GPIO_OUT通知GPIO_IN。

中断依赖qemu_irq结构体

struct IRQState {
o bject parent_obj; qemu_irq_handler handler; // irq处理函数
void *opaque;
int n; // irq的编号
};
typedef struct IRQState *qemu_irq;

要触发一个irq,可以使用qemu_set_irq函数

void qemu_set_irq(qemu_irq irq, int level)
{
if (!irq)
return; irq->handler(irq->opaque, irq->n, level); // 调用irq的回调函数,传入中断号n
}

GPIO_IN通过qdev_init_gpio_in初始化

void qdev_init_gpio_in(DeviceState *dev, qemu_irq_handler handler, int n)

初始化n个GPIO_IN接口,每个GPIO_IN接口的回调函数为handler,实际就是新建n个qemu_irq对象,qemu_irq的回调函数为handler。

GPIO_OUT初始化函数为sysbus_init_irq

/* Request an IRQ source.  The actual IRQ o bject may be populated later.  */
void sysbus_init_irq(SysBusDevice *dev, qemu_irq *p)

qemu使用sysbus_connect_irqGPIO_OUTGPIO_IN关联

void sysbus_connect_irq(SysBusDevice *dev, int n, qemu_irq irq)
把dev中的第n个gpio_out和irq关联
实际就是把irq保存为第n个gpio_out的值
实例分析

比如在armv7m_nvic_realize调用qdev_init_gpio_in初始化num_irq个GPIO_IN

static void armv7m_nvic_realize(DeviceState *dev, Error **errp)
{
qdev_init_gpio_in(dev, set_irq_level, s->num_irq);

uart设备在stm32f2xx_usart_init函数中通过sysbus_init_irq初始化一个GPIO_OUT

 sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);

这样第0个GPIO就指向了s->irq

stm32f205_soc_realize会使用sysbus_connect_irq把设备的第0个GPIO和 nvic 的特定GPIO_IN进行关联。

实质上就是把 s->irq = qdev_get_gpio_in(armv7m, timer_irq[i])

sysbus_connect_irq(busdev, 0, qdev_get_gpio_in(armv7m, usart_irq[i]));

timer_irq 保存了每个uart设备需要使用的IRQ号

static const int usart_irq[STM_NUM_USARTS] = {37, 38, 39, 52, 53, 71};

此外还有一个需要注意的点,这里的irq号和其在异常向量表中的位置存在以下关系

IRQ 号 = IRQ处理函数在异常向量表中的序号 - CPU内置异常数目

以stm32f205-soc为例,其使用的CPU为cortex-m3,CPU的内部中断数目为16个

比如异常向量表的第17号中断的irq编号为 17 - 16 = 1,下图是设备手册异常向量表中IRQ开头部分:

uart设备在stm32f2xx_usart_write中需要触发特定中断时会调用

            if (s->usart_cr1 & USART_CR1_RXNEIE &&
s->usart_sr & USART_SR_RXNE) {
qemu_set_irq(s->irq, 1);
}

s->irq 在之前使用sysbus_connect_irq时就被设置成nvic中对应irq的qemu_irq结构

这里实际会调用set_irq_level通知nvic指定的中断到来

/* callback when external interrupt line is changed */
static void set_irq_level(void *opaque, int n, int level)
{
n += NVIC_FIRST_IRQ; // irq 号 + CPU内置异常树(16)
vec = &s->vectors[n];
if (level != vec->level) {
vec->level = level;
if (level) {
armv7m_nvic_set_pending(s, n, false);
}
}
}

主要就是根据IRQ号n,找到对应的异常信息 vec, 然后判断vec的状态(高定平(level=1),还是低电平(level=0))

如果是高电平,则会进入armv7m_nvic_set_pending通知CPU中断到来,实际也是调用CPU之前注册的GPIO_IN的回调函数通知。

因此qemu的中断实现其实是依赖于qemu_irq来实现,比如NVIC要通知CPU中断到来,实际就是调用CPU的qemu_irq中的回调函数实现。

固件加载

netduino2_init在初始化stm32f205-soc后,调用armv7m_load_kernel加载二进制到内存

armv7m_load_kernel(ARM_CPU(first_cpu), machine->kernel_filename, FLASH_SIZE);

void armv7m_load_kernel(ARMCPU *cpu, const char *kernel_filename, int mem_size)
{
..................
if (kernel_filename) {
image_size = load_elf_as(kernel_filename, NULL, NULL, NULL,
&entry, &lowaddr,
NULL, big_endian, EM_ARM, 1, 0, as);
if (image_size < 0) {
image_size = load_image_targphys_as(kernel_filename, 0,
mem_size, as);
lowaddr = 0;
} } qemu_register_reset(armv7m_reset, cpu);
}
  1. machine->kernel_filename通过命令的 -kernel 选项指定

  2. armv7m_load_kernel首先尝试调用load_elf_as以elf格式加载

  3. 如果加载失败,就调用 load_image_targphys_as 直接把文件加载到0地址处

裸板程序和IRQ请求调试

本节基于stm32f205-soc的进行修改实现QEMU对中断的模拟,然后开发裸板程序对模拟的中断进行验证。

stm32f205-soc修改

qemu_irq stm32f2xx_irq_demo_handler = NULL;
#define IRQ_DEMO_b ase 0x88990000 static void stm32f2xx_irq_demo_write(void *opaque, hwaddr addr,
uint64_t val64, unsigned int size)
{
qemu_set_irq(stm32f2xx_irq_demo_handler, 1);
return;
} static const MemoryRegionOps stm32f2xx_irq_demo_ops = {
.write = stm32f2xx_irq_demo_write,
.endianness = DEVICE_NATIVE_ENDIAN,
}; static void stm32f205_soc_realize(DeviceState *dev_soc, Error **errp)
{
memory_region_init_io(demo_mem, NULL, &stm32f2xx_irq_demo_ops, s,
"irq-demo-mmio", 0x1000);
memory_region_add_subregion(system_memory, IRQ_DEMO_b ase, demo_mem); stm32f2xx_irq_demo_handler = qdev_get_gpio_in(armv7m, 20); // 拿到nvic的irq 20 的 irq
  1. 首先在stm32f205_soc_realize中获取IRQ为20的NVIC.GPIO_IN,即其对应的qemu_irq结构,然后保存到stm32f2xx_irq_demo_handler中
  2. 注册0x88990000处内存写回调函数为stm32f2xx_irq_demo_write
  3. 当往0x88990000写数据时会进入stm32f2xx_irq_demo_write
  4. 在stm32f2xx_irq_demo_write函数中会调用qemu_set_irq触发 IRQ 20 中断

裸板程序

根据手册定义异常向量表,当系统启动时会调用Reset_Handler,当IRQ-20中断触发时会进入demo_irq_handler

// ISR vecotor data
.section .isr_vector, "a"
g_pfnVectors:
.word stack_top
.word Reset_Handler
.word Default_Handler // NMI
.word Default_Handler // HardFault
.word Default_Handler // MemManage
.word Default_Handler // BusFault
.word Default_Handler // UsageFault
.word 0
.word 0
.word 0
.word 0
.word Default_Handler // SVC
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word demo_irq_handler // CAN1_RX0 for demo_irq

中断处理程序定义

.thumb_func
Reset_Handler:
@ MOV R0, #0
@ MSR PRIMASK, R0
bl main_func
b . .thumb_func
demo_irq_handler:
bl demo_irq_func
b .

相关函数实现

#define USART1_b ase_ADDR 0x40011000
#define USART_DR 0x04 #define IRQ_DEMO_b ase 0x88990000 #define NVIC_MEM_b ase 0xe000e000 void print_func(unsigned char* s)
{
while (*s != 0)
{
*(volatile unsigned char*)(USART1_b ase_ADDR + USART_DR) = *s;
s++;
}
} void enable_demo_irq()
{
unsigned int irq = 20 + 16;
unsigned int offset = (irq - 16) / 8;
offset += 0x180;
offset -= 0x80;
*(volatile unsigned char*)(NVIC_MEM_b ase + offset) = 1 << 4;
} void main_func()
{
print_func("main_func!\n");
enable_demo_irq(); // 配置 nvic 的mmio,让 20号 irq 的 enabled=1 *(volatile unsigned int*)(IRQ_DEMO_b ase + 4) = 33; // 触发 demo_irq, 下面进入 demo_irq_func
print_func("end main_func!\n");
return;
} void demo_irq_func()
{
print_func("demo_irq_func!\n");
return;
}

print_func函数

通过写UART的内存实现输出

main函数

  1. 首先打印一个日志,然后调用enable_demo_irq设置 nvic 控制器,让IRQ-20启用
  2. 然后触发对IRQ_DEMO_b ase内存的写,让qemu端触发IRQ-20中断

demo_irq_func函数

打印日志

使用qemu加载固件执行的输出如下

$ qemu-system-arm -M netduino2 -kernel startup.bin -nographic
main_func!
demo_irq_func!

可以看到首先进入了main函数,触发IRQ-20中断后进入了demo_irq_func。

注意

由于系统启动时NVIC中每个异常向量的enable状态为0,从而导致即使使用qemu_set_irq通知中断到来,实际也不会被CPU处理.


static MemTxResult nvic_sysreg_write(void *opaque, hwaddr addr,
uint64_t value, unsigned size,
MemTxAttrs attrs)
{
switch (offset) {
case 0x100 ... 0x13f: /* NVIC Set enable */
offset += 0x80;
setval = 1;
/* fall through */
case 0x180 ... 0x1bf: /* NVIC Clear enable */
startvec = 8 * (offset - 0x180) + NVIC_FIRST_IRQ;
for (i = 0, end = size * 8; i < end && startvec + i < s->num_irq; i++) {
if (value & (1 << i) &&
(attrs.secure || s->itns[startvec + i])) {
s->vectors[startvec + i].enabled = setval;
}
}
nvic_irq_update(s);

所以在触发中断前要通过写NVIC的MMIO内存来设置NVIC中异常向量的enable为1

void enable_demo_irq()
{
unsigned int irq = 20 + 16;
unsigned int offset = (irq - 16) / 8;
offset += 0x180;
offset -= 0x80;
*(volatile unsigned char*)(NVIC_MEM_b ase + offset) = 1 << 4;
}

总结

本文以stm32f205-soc为例子介绍了针对真实硬件仿真的实现,并分析了qemu的中断模型,最后给出仿真中断的例子。

参考链接

https://blog.csdn.net/alex_mianmian/article/d etails/98174812
https://www.cnblogs.com/utank/p/11304226.html

QEMU固件模拟技术-stm32仿真分析及IRQ仿真实践的更多相关文章

  1. STM32 KEIL不能输入仿真引脚端口error 65: access violation at 0x40021000 : no 'read' permission

    使用MDK自己创建一个STM32F103ZE核的项目 加入源码后编译,正常,在线仿真单步执行出现如下问题 error 65: access violation at 0x40021000 : no ' ...

  2. STM32之RGB灯仿真

    实验目的 点灯是练习GPIO输出的最佳实验.由于疫情期间没法返校,手头上没有现成的实验板,于是借助Proteus进行仿真.本实验点的不是普通的灯,而是RGB混色灯,实现多种颜色的显示.后期还可以加上P ...

  3. Cadence 电源完整性仿真实践(一)

    软件版本号:Cadence 16.5 使用工具:Allegro PCB PI Option XL Power Integrity 使用资源:仿真实例下载地址:http://download.csdn. ...

  4. 基于eNSP的NAT/NAPT协议仿真实践

    一. 基本原理 eNSP(Enterprise Network Simulation Platform)是一款由华为提供的.可扩展的.图形化 操作的网络仿真工具平台,主要对企业网络路由器.交换机进行软 ...

  5. STM32常见问题

    一.STM32 下不了程序 提示: (1).JLink Info: CPU halted Erase Done. Programming Failed! 解决办法: 用isp把芯片全部擦除再试试,如果 ...

  6. QEMU, a Fast and Portable Dynamic Translator-Fabrice Bellard-翻译

    Abstract We present the internals of QEMU, a fast machine emulator using an original portable dynami ...

  7. qemu源码分析

    参考:http://lists.gnu.org/archive/html/qemu-devel/2011-04/pdfhC5rVdz7U8.pdf 1. qemu与Bochs的区别: 1. Bochs ...

  8. 72.xilinx vivado zynq vdma仿真及应用详解(一)

    很多人用zynq平台做视频图像开发,但是对vdma了解比较少,上手起来稍微有些困难,我针对这一现象,做了一个基于vivado和modelsim的仿真和应用测试工程,并写篇文章做些介绍,希望能对大家有帮 ...

  9. 实验七 状态机设计ADC0809采样控制电路

    一.实验目的 学习用状态机实现A/D转换器ADC0809的采样控制电路. 二.实验内容 利用QuartusⅡ实现A/D转换器ADC0809的采样控制电路状态机设计:给出仿真波形.最后进行引脚锁定并进行 ...

  10. 实验六 序列信号检测器的VHDL设计

    一.实验目的 (1)进一步熟悉Quartus II软件和GW48-PK2S实验系统的使用方法: (2)用状态机实现序列检测器的设计,了解一般状态机的设计与应用 二.实验内容 1. 基本命题 利用Qua ...

随机推荐

  1. 【QT性能优化】QT性能优化之QT性能优化实战 QML优化 QT高性能 QT6系列视频课程 QT6 性能优化实战 QT高性能 QT原理源码 QML优化 GUI绘图原理源码

    QT性能优化实战视频课程 QT6 Widgets高性能应用编程 1.课前考试 2.字符串优化(上) 3.字符串优化(下) 4.绘图优化(上) 5.绘图优化(下)  6.QT界面优化(上) 7.QT界面 ...

  2. 彻底理解 IP 地址,子网掩码,子网划分

    原文地址:https://oldme.net/article/55彻底理解 IP 地址,子网掩码,子网划分 什么是 IP 协议 在回答什么是 IP 协议前,我们先需要回答另外一个问题:什么是网络?从普 ...

  3. 仿Linux内核链表实现合并有序链表、逆序单链表功能,C版本 JavaScript版本

    直接贴上已经码好的: list_sort.c: #include <stdio.h> #include <string.h> #include <assert.h> ...

  4. 2.2.2 PyTorch 2.0 GPU NVIDIA运行库的安装 ——CUDA+cuDNN安装教程

    参考文章: https://blog.csdn.net/mbdong/article/details/121769951 CUDA download: https://developer.nvidia ...

  5. 通过MobaXterm操作远程服务器电脑的方法

      本文介绍在Windows电脑中,下载.配置MobaXterm软件,从而连接.操作远程服务器的方法.   因为是在Windows操作系统的电脑中连接服务器,所以建议使用MobaXterm.PuTTY ...

  6. JS时间对象与字符串相互转换

    1.Date => String 代码 /** * 函数描述:时间格式化工具 * @param format {String} 格式(y-年,M-月,d-日,H-时[24],h-时[12],m- ...

  7. 《你必须掌握的Entity Framework 6.x与Core 2.0》代码下载

      https://item.jd.com/12376014.html<你必须掌握的Entity Framework 6.x与Core 2.0>代码下载,共享代码: https://git ...

  8. C#/.NET/.NET Core优秀项目和框架2024年9月简报

    前言 公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍.功能特点.使用方式以及部分功能截图 ...

  9. python中字典的运算

    问题: 如何查找在两个字典中相同的键.值元素? dict1 = {'a': 1, 'b': 2, 'c': 3} dict2 = {'a': 10, 'y': 11,'b': 2} dict1.key ...

  10. appium环境搭建及命令行启动sdk模拟器-附踩坑以及解决过程

    安装教程这里就不阐述了,网上一大堆教程,下载完成后安装然后配置对应的环境变量即可 android sdk及java home配置: path配置: %ANDROID_HOME%\platform-to ...