文章首发于

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. ubuntu22.04安装DBeaver

    要在 Ubuntu22.04 上安装 DBeaver,可以选择使用 Ubuntu 软件中心的图形界面方法或使用命令行方法通过官方 DBeaver 仓库或 Snap 包安装. 方法一:从官方仓库安装 D ...

  2. 5G网元功能与接口

    5G网元功能与接口 5G移动通信整体网络架构 5G网络功能之间的信息交互可以基于两种方式表示:其一为基于服务表示:其二为基于点对点表示.实时部署时,也可以采用两种方式相结合的表示方式. 并不是所有的接 ...

  3. .NET 开源 EF Core 批处理扩展工具,真好用

    前言 Entity Framework Core(EF Core)作为 .NET 生态系统中受欢迎的对象关系映射器(ORM),其轻量级.可扩展性和支持多个数据库引擎而备受青睐. 本文将介绍一款.NET ...

  4. DWC3和XHCI的区别

    DWC3(DesignWare USB 3.0 Controller)和XHCI(eXtensible Host Controller Interface)都是与USB控制器相关的技术,但它们的作用和 ...

  5. 80篇国产数据库实操文档汇总(含TiDB、达梦、openGauss等)

    国产数据库发展得如火如荼,数据库的国产化替代也正在进行中.最近,有越来越多的朋友都加入了学习国产数据库的队伍中,本文便选取了墨天轮技术社区的国产数据库流行度排行榜上排名靠前的几个数据库,整理了相关的实 ...

  6. 11-react使用props.children 处理父子组件之间的传值

    // props.children 组件传值 import { Component } from "react" import reactDom from "react- ...

  7. 全网最适合入门的面向对象编程教程:57 Python字符串与序列化-序列化与反序列化

    全网最适合入门的面向对象编程教程:57 Python 字符串与序列化-序列化与反序列化 摘要: Python 序列化与反序列化是将 Python 对象转换为字节流(序列化)以便存储或传输,和将字节流转 ...

  8. 【转】sqlplus/RMAN/lsnrctl 等工具连接缓慢

    AIX上sqlplus /as sysdba rman target / 或者lsnrctl start时或者通过sqlplus system/oracle@orcl这样通过监听连接等方式来登陆时非常 ...

  9. 云原生周刊:Prometheus 3.0 Beta 发布|2024.09.16

    开源项目推荐 Kuma Kuma 是一个现代化的基于 Envoy 的服务网格,能够在每个云平台上运行,支持单区域或多区域部署,兼容 Kubernetes 和虚拟机.凭借其广泛的通用工作负载支持,以及对 ...

  10. 在 KubeSphere 中使用 Rook 构建云原生存储环境

    Rook 介绍 Rook 是一个开源的云原生存储编排器,为各种存储解决方案提供平台.框架和支持,以便与云原生环境进行原生集成. Rook 将分布式存储系统转变为自管理.自扩展.自修复的存储服务.它使存 ...