文章目录

  • 1. MY_SOC Memory Map
  • 2. MY_SOC 源码
  • 3. 测试代码
  • 4 创建自己的IP
  • 4.1 memory map
  • 4.2 my_test_ip源码
  • 4.3 测试代码


源码文件

随着国内芯片公司越来越多,越来越多的底层程序员需要在pre silicon阶段就要开发代码。而在pre silicon阶段测试方式有多种:

方式

优点

缺点

RTL simulation

可以验证最准确的硬件行为,可以测试SOC相关代码

仿真速度非常慢,且rtl freeze之前硬件有bug

FPGA/ZEBU emulation

可以验证部分硬件行为,速度相对RTL simulation快

价格昂贵,难以布署大量测试,且有些硬件没法仿真

软件模拟器,如QEMU

速度最快,可以布署大量测试

没有现成的模拟器对应正在开发的SOC

很明显软件模拟器的优点缺点显而易见,如果开发人员可以在芯片开发前期定制出一个软件模拟器对应SOC 原型,那么可以大大提高pre silicon的效率。本文以一个小demo来演示如何在qemu源码基础上搭建一个最简单的Cortex M4的SOC。 这里就不讲如何编译qemu-system-arm了,网上有很多教程教学如何编译qemu-system-arm。后文简称该SOC为MY_SOC

1. MY_SOC Memory Map

Cortex M自带的system peripheral的地址这里就不列出来了,比如systick,nvic这些都是arm规定的,没法改,也没必要改。这里定义了最简单三个外设。一段FLASH用来跑代码,一段SRAM用来存数据,一个UART来打印。

起始地址

结束地址

FLASH(定义了一段4M 的FLASH)

0x00000000

0x003FFFFF

SRAM(定义了一段16M的SRAM)

0x20000000

0x20FFFFFF

UART0(使用ARM PL011 IP)

0x40000000

0x40000FFF

2. MY_SOC 源码

将my_soc.c 放在qemu/hw/arm目录下并且加入arm的makefile编译即可。先贴出全部代码再逐行解释。加上头文件include和宏定义一共77行代码,可见利用qemu能够很方便地搭出一个SOC模拟器原型。

#include "qemu/osdep.h"
  2 #include "qapi/error.h"
  3 #include "hw/arm/boot.h"
  4 #include "hw/boards.h"
  5 #include "qemu/log.h"
  6 #include "exec/address-spaces.h"
  7 #include "sysemu/sysemu.h"
  8 #include "hw/arm/armv7m.h"
  9 #include "hw/char/pl011.h"
 10 #include "hw/irq.h"
 11 #include "cpu.h"
 12 
 13 #define MY_SOC_FLASH_START  (0x0)
 14 #define MY_SOC_FLASH_SIZE   (4 * 1024 * 1024) //< 4M
 15 
 16 #define MY_SOC_SRAM_START    (0x20000000)
 17 #define MY_SOC_SRAM_SIZE     (16 * 1024 * 1024) //<16M
 18 
 19 #define PL011_UART0_START   (0x40000000)
 20 #define PL011_UART0_IRQn    (0)
 21 
 22 #define NUM_IRQ_LINES 64
 23 
 24 static void mysoc_init(MachineState *ms)
 25 {
 26     DeviceState *nvic;
 27 
 28     MemoryRegion *sram = g_new(MemoryRegion, 1);
 29     MemoryRegion *flash = g_new(MemoryRegion, 1);
 30     MemoryRegion *system_memory = get_system_memory();
 31     
 32     /* Flash programming is done via the SCU, so pretend it is ROM.  */
 33     memory_region_init_ram(flash, NULL, "mysoc.flash", MY_SOC_FLASH_SIZE, &error_fatal);
 34     memory_region_set_readonly(flash, true);
 35     memory_region_add_subregion(system_memory, MY_SOC_FLASH_START, flash);
 36     
 37     memory_region_init_ram(sram, NULL, "mysoc.sram", MY_SOC_SRAM_SIZE, &error_fatal);
 38     memory_region_add_subregion(system_memory, MY_SOC_SRAM_START, sram);
 39     
 40     nvic = qdev_create(NULL, TYPE_ARMV7M);
 41     qdev_prop_set_uint32(nvic, "num-irq", NUM_IRQ_LINES);
 42     qdev_prop_set_string(nvic, "cpu-type", ms->cpu_type);
 43     qdev_prop_set_bit(nvic, "enable-bitband", true);
 44     object_property_set_link(OBJECT(nvic), OBJECT(get_system_memory()), "memory", &error_abort);
 45     
 46     /* This will exit with an error if the user passed us a bad cpu_type */
 47     qdev_init_nofail(nvic);
 48     
 49     pl011_luminary_create(PL011_UART0_START , qdev_get_gpio_in(nvic, PL011_UART0_IRQn), serial_hd(0));
 50     armv7m_load_kernel(ARM_CPU(first_cpu), ms->kernel_filename, MY_SOC_FLASH_SIZE);
 51 }
 52 
 53 
 54 static void mysoc_class_init(ObjectClass *oc, void *data)
 55 {
 56     MachineClass *mc = MACHINE_CLASS(oc);
 57     printf("%s entry\n", __func__);
 58 
 59     mc->desc = "My SOC Cortex M4";
 60     mc->init = mysoc_init;
 61     mc->ignore_memory_transaction_failures = true;
 62     mc->default_cpu_type = ARM_CPU_TYPE_NAME("cortex-m4");
 63 }
 64 
 65 static const TypeInfo mysoc_type = {
 66     .name = MACHINE_TYPE_NAME("mysoc_evb"),
 67     .parent = TYPE_MACHINE,
 68     .class_init = mysoc_class_init,
 69 };
 70 
 71 static void mysoc_evb_init(void)
 72 {
 73     type_register_static(&mysoc_type);
 74 }
 75 
 76 type_init(mysoc_evb_init)
 77

代码很简单,从下往上看。这里定义了一块板子叫mysoc_evb,通过type_init宏上报给qemu,之后qemu在启动地时候就能自动地调用mysoc_init初始化soc外设。

这里定义了描述字符串为My SOC Cortex M4,cpu类型是cortex-m4,板子名字是mysoc_evb。当这些结构体初始化完后,运行qemu-system-arm -machine help 就会出现我们自己地设备。

qemu 指定内存地址 qemu自定义硬件_#define

54 static void mysoc_class_init(ObjectClass *oc, void *data)
 55 {
 56     MachineClass *mc = MACHINE_CLASS(oc);
 57     printf("%s entry\n", __func__);
 58 
 59     mc->desc = "My SOC Cortex M4";
 60     mc->init = mysoc_init;
 61     mc->ignore_memory_transaction_failures = true;
 62     mc->default_cpu_type = ARM_CPU_TYPE_NAME("cortex-m4");
 63 }
 64 
 65 static const TypeInfo mysoc_type = {
 66     .name = MACHINE_TYPE_NAME("mysoc_evb"),
 67     .parent = TYPE_MACHINE,
 68     .class_init = mysoc_class_init,
 69 };
 70 
 71 static void mysoc_evb_init(void)
 72 {
 73     type_register_static(&mysoc_type);
 74 }
 75 
 76 type_init(mysoc_evb_init)

所以最关键地函数就是mysoc_init这个函数,这个函数里做的事情非常简单,就是调用qemu的API创建相应的memory map以及设备就可以了。

  • 创建4M flash并设置为只读,定义了一个默认的加载文件mysoc.flash。如果mysoc.flash存在。那么qemu就会把该文件的数据读到这段flash中
33     memory_region_init_ram(flash, NULL, "mysoc.flash", MY_SOC_FLASH_SIZE, &error_fatal);
 34     memory_region_set_readonly(flash, true);                                       
 35     memory_region_add_subregion(system_memory, MY_SOC_FLASH_START, flash);
  • 创建一段16M的SRAM, 并定义默认文件mysoc.sram
37     memory_region_init_ram(sram, NULL, "mysoc.sram", MY_SOC_SRAM_SIZE, &error_fatal);
 38     memory_region_add_subregion(system_memory, MY_SOC_SRAM_START, sram);
  • 配置NVIC, 设置64个外部中断槽,使能bitband。
40     nvic = qdev_create(NULL, TYPE_ARMV7M);
 41     qdev_prop_set_uint32(nvic, "num-irq", NUM_IRQ_LINES);                          
 42     qdev_prop_set_string(nvic, "cpu-type", ms->cpu_type);                          
 43     qdev_prop_set_bit(nvic, "enable-bitband", true);
 44     object_property_set_link(OBJECT(nvic), OBJECT(get_system_memory()), "memory", &error_abort);
  • 添加一个PL011 UART,地址为0x40000000, 中断号为0。
49     pl011_luminary_create(PL011_UART0_START , qdev_get_gpio_in(nvic, PL011_UART0_IRQn), serial_hd(0));
  • 设置kernel加载到flash中
50     armv7m_load_kernel(ARM_CPU(first_cpu), ms->kernel_filename, MY_SOC_FLASH_SIZE);

至此MY_SOC就配置完了。我们写一段测试代码来测试这个模拟器能不能运行。

3. 测试代码

链接文件,把代码段放在FLASH中,data,bss段放在SRAM中

MEMORY
{
    FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 4M
    SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 16M
}

SECTIONS
{
    .text :
    {
        _text = .;
        KEEP(*(.isr_vector))
        *(.text*)
        *(.rodata*)
        _etext = .;
    } > FLASH

    /DISCARD/ :
    {
        *(.ARM.exidx*)
        *(.gnu.linkonce.armexidx.*)
    }

    .data : AT(ADDR(.text) + SIZEOF(.text))
    {
        _data = .;
        *(vtable)
        *(.data*)
        _edata = .;
    } > SRAM

    .bss :
    {
        _bss = .;
        *(.bss*)
        *(COMMON)
        _ebss = .;
    } > SRAM

    . = ALIGN(32);           /*Not sure if this needs to be done, but why not.*/
    _stack_bottom = .;       /*Address of the bottom of the stack.*/
    . = . + 0x4000;          /*Allocate 4K for the Stack.*/
    _stack_top = 0x20008000;  /*Address of the top of the heap, also end of RAM.*/
}

startup.c 定义了栈指针和函数入口main

__attribute__ ((section(".isr_vector")))void (*g_pfnVectors[])(void) =
{
    0x20008000,                                      // StackPtr, set in RestetISR
    main,                               // The reset handler
    NmiSR,                                  // The NMI handl

main函数里就是往UART0里输出了Hello My SOC

#include <stdint.h>

static volatile uint32_t * const UART0_DR = (uint32_t *)0x40000000;

void puts(char *str)
{
    while(*str != 0) {
        *UART0_DR = *str;
        str++;
    }
}

int main()
{

    puts("Hello My SOC\n");
    while(1);
    return 0;
}

编译运行看结果

qemu 指定内存地址 qemu自定义硬件_qemu 指定内存地址_02

4 创建自己的IP

qemu/hw下面已经内置了许多可用的外设IP,如果有就可以简单地直接在像搭积木一样地配置一下就行。但如果在SOC开发中加入了一些自研的IP, 而此时在hw下面并没有,这个时候就需要自己加入IP的模拟器代码。本小节以一个最简单的读写寄存器来示范如何在qemu模拟器中加入自己的IP。

4.1 memory map

register

offset

reset_value

R/W

ID0

0x0

0x54 (T)

RO

ID1

0x4

0045 (E)

RO

ID2

0x8

0x53 (S)

RO

ID3

0xc

0x54 (T)

RO

ID4

0x10

0x20 (Space)

RO

ID5

0x14

0x40 (I)

RO

ID6

0x18

0x50 §

RO

Test Reg

0x1c

0

RO

寄存器功能很简单,前面7个是只读ID寄存器,最后一个是可读写的寄存器,没什么实际作用,就是demo用。没有中断产生。

4.2 my_test_ip源码

25 #define MY_TEST_IP_START    (0x40001000)
 26 
 27 #define NUM_IRQ_LINES 64
 28 
 29 typedef struct {
 30     SysBusDevice parent_obj;
 31 
 32     qemu_irq irq;
 33     MemoryRegion iomem;
 34     uint32_t id0;       //T
 35     uint32_t id1;       //E
 36     uint32_t id2;       //S
 37     uint32_t id3;       //T
 38     uint32_t id4;       //
 39     uint32_t id5;       //I
 40     uint32_t id6;       //P
 41     uint32_t test_reg;
 42 } my_test_ip_state;
106 #define TEST_IP(obj) \
107     OBJECT_CHECK(my_test_ip_state, (obj), TYPE_TEST_IP)
108     
109 
110 static const VMStateDescription my_test_ip_vm = {
111     .name = "my_test_ip",
112     .version_id = 1,
113     .minimum_version_id = 1,
114     .fields = (VMStateField[]) {
115         VMSTATE_UINT32(id0, my_test_ip_state),
116         VMSTATE_UINT32(id1, my_test_ip_state),
117         VMSTATE_UINT32(id2, my_test_ip_state),
118         VMSTATE_UINT32(id3, my_test_ip_state),
119         VMSTATE_UINT32(id4, my_test_ip_state),
120         VMSTATE_UINT32(id5, my_test_ip_state),
121         VMSTATE_UINT32(id6, my_test_ip_state),
122         VMSTATE_UINT32(test_reg, my_test_ip_state),
123         VMSTATE_END_OF_LIST()
124     }
125 };
126 
127 static uint64_t my_test_ip_read(void *opaque, hwaddr offset,
128                                    unsigned size)
129 {   
130     uint64_t ret = 0;
131     my_test_ip_state *s = (my_test_ip_state *)opaque;
132     printf("%s hwaddr:%lx, size:%x\n", __func__, offset, size);
133     switch (offset) {
134     case 0x0:
135         ret = s->id0;
136         break;
137     case 0x4:
138         ret = s->id1; 
139         break;
140     case 0x8: 
141         ret = s->id2;
142         break;
143     case 0xc:
144         ret = s->id3;
145         break;
146     case 0x10:
147         ret = s->id4;
148         break;
149     case 0x14:
150         ret = s->id5;
151         break;
152     case 0x18:
153         ret = s->id6;
154         break;
155     case 0x1c:
156         ret = s->test_reg;
157         break;
158     }
159     return ret;
160 }
161 
162 static void my_test_ip_write(void *opaque, hwaddr offset,
163                                 uint64_t value, unsigned size)
164 {
165 
166     my_test_ip_state *s = (my_test_ip_state *)opaque;
167     printf("%s hwaddr:%lx, size:%x, value:%lx\n", __func__, offset, size, value);
168     switch(offset){
169     case 0x0:
170     case 0x4:
171     case 0x8:
172     case 0xc:
173     case 0x10:
174     case 0x14:
175     case 0x18:
176         printf("%s: cannot write the read only register\n", __func__);
177         break;
178     case 0x1c:
179         s->test_reg = value;
180         break;
181     }
182 }
183 
184 static const MemoryRegionOps my_test_ip_ops = {
185     .read = my_test_ip_read,
186     .write = my_test_ip_write,
187     .endianness = DEVICE_NATIVE_ENDIAN,
188 };
189 
190 static void my_test_ip_init(Object *obj)
191 {
192     printf("%s \n", __func__);
193 
194     my_test_ip_state *s = TEST_IP(obj);
195 
196     memory_region_init_io(&s->iomem, obj, &my_test_ip_ops, s, TYPE_TEST_IP, 0x1000);
197     sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->iomem);
198 
199     s->id0 = 0x54;
200     s->id1 = 0x45;
201     s->id2 = 0x53;
202     s->id3 = 0x54;
203     s->id4 = 0x20;
204     s->id5 = 0x49;
205     s->id6 = 0x50;
206     s->test_reg = 0;
207 }
208 
209 static void my_test_ip_class_init(ObjectClass *klass, void *data)
210 {
211     printf("%s \n", __func__);
212     DeviceClass *dc = DEVICE_CLASS(klass);
213     dc->vmsd = &my_test_ip_vm;
214 }
215 
216 static const TypeInfo my_test_ip = {
217     .name          = TYPE_TEST_IP,
218     .parent        = TYPE_SYS_BUS_DEVICE,
219     .instance_size = sizeof(my_test_ip_state),
220     .instance_init = my_test_ip_init,
221     .class_init    = my_test_ip_class_init,
222 };
223 
224 static void my_test_ip_types(void)
225 {
226     printf("%s \n", __func__);
227     type_register_static(&my_test_ip);
228 }
229 
230 type_init(my_test_ip_types)

4.3 测试代码

#define MY_TEST_IP_START 0x40001000
typedef struct my_ip_tag {
    volatile uint32_t id0;
    volatile uint32_t id1;
    volatile uint32_t id2;
    volatile uint32_t id3;
    volatile uint32_t id4;
    volatile uint32_t id5;
    volatile uint32_t id6;
    volatile uint32_t test_reg;
} my_test_ip_t;

void my_test_ip_sample()
{
    my_test_ip_t *ip = (my_test_ip_t *)MY_TEST_IP_START;
    char id[8];
    id[0] = (char) ip->id0;
    id[1] = (char) ip->id1;
    id[2] = (char) ip->id2;
    id[3] = (char) ip->id3;
    id[4] = (char) ip->id4;
    id[5] = (char) ip->id5;
    id[6] = (char) ip->id6;
    id[7] = 0;

    puts(id);
    puts("\n");

    ip->test_reg = 0x00414141;
    id [0] = ip->test_reg & 0xff;
    id [1] = (ip->test_reg & 0xff00) >> 8;
    id [2] = (ip->test_reg & 0xff0000) >> 16;
    id [3] = (ip->test_reg & 0xff000000) >> 24;
    puts(id);
    puts("\n");
}

运行结果: 红框内是qemu打印的,蓝框是测试程序打印出来的,可以看到能顺利读出ID,同时能读写test reg。

qemu 指定内存地址 qemu自定义硬件_IP_03