1 | /* |
2 | * QEMU RISC-V Spike Board |
3 | * |
4 | * Copyright (c) 2016-2017 Sagar Karandikar, sagark@eecs.berkeley.edu |
5 | * Copyright (c) 2017-2018 SiFive, Inc. |
6 | * |
7 | * This provides a RISC-V Board with the following devices: |
8 | * |
9 | * 0) HTIF Console and Poweroff |
10 | * 1) CLINT (Timer and IPI) |
11 | * 2) PLIC (Platform Level Interrupt Controller) |
12 | * |
13 | * This program is free software; you can redistribute it and/or modify it |
14 | * under the terms and conditions of the GNU General Public License, |
15 | * version 2 or later, as published by the Free Software Foundation. |
16 | * |
17 | * This program is distributed in the hope it will be useful, but WITHOUT |
18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
19 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
20 | * more details. |
21 | * |
22 | * You should have received a copy of the GNU General Public License along with |
23 | * this program. If not, see <http://www.gnu.org/licenses/>. |
24 | */ |
25 | |
26 | #include "qemu/osdep.h" |
27 | #include "qemu/log.h" |
28 | #include "qemu/error-report.h" |
29 | #include "qapi/error.h" |
30 | #include "hw/boards.h" |
31 | #include "hw/loader.h" |
32 | #include "hw/sysbus.h" |
33 | #include "target/riscv/cpu.h" |
34 | #include "hw/riscv/riscv_htif.h" |
35 | #include "hw/riscv/riscv_hart.h" |
36 | #include "hw/riscv/sifive_clint.h" |
37 | #include "hw/riscv/spike.h" |
38 | #include "hw/riscv/boot.h" |
39 | #include "chardev/char.h" |
40 | #include "sysemu/arch_init.h" |
41 | #include "sysemu/device_tree.h" |
42 | #include "sysemu/qtest.h" |
43 | #include "sysemu/sysemu.h" |
44 | #include "exec/address-spaces.h" |
45 | |
46 | #include <libfdt.h> |
47 | |
48 | static const struct MemmapEntry { |
49 | hwaddr base; |
50 | hwaddr size; |
51 | } spike_memmap[] = { |
52 | [SPIKE_MROM] = { 0x1000, 0x11000 }, |
53 | [SPIKE_CLINT] = { 0x2000000, 0x10000 }, |
54 | [SPIKE_DRAM] = { 0x80000000, 0x0 }, |
55 | }; |
56 | |
57 | static void create_fdt(SpikeState *s, const struct MemmapEntry *memmap, |
58 | uint64_t mem_size, const char *cmdline) |
59 | { |
60 | void *fdt; |
61 | int cpu; |
62 | uint32_t *cells; |
63 | char *nodename; |
64 | |
65 | fdt = s->fdt = create_device_tree(&s->fdt_size); |
66 | if (!fdt) { |
67 | error_report("create_device_tree() failed" ); |
68 | exit(1); |
69 | } |
70 | |
71 | qemu_fdt_setprop_string(fdt, "/" , "model" , "ucbbar,spike-bare,qemu" ); |
72 | qemu_fdt_setprop_string(fdt, "/" , "compatible" , "ucbbar,spike-bare-dev" ); |
73 | qemu_fdt_setprop_cell(fdt, "/" , "#size-cells" , 0x2); |
74 | qemu_fdt_setprop_cell(fdt, "/" , "#address-cells" , 0x2); |
75 | |
76 | qemu_fdt_add_subnode(fdt, "/htif" ); |
77 | qemu_fdt_setprop_string(fdt, "/htif" , "compatible" , "ucb,htif0" ); |
78 | |
79 | qemu_fdt_add_subnode(fdt, "/soc" ); |
80 | qemu_fdt_setprop(fdt, "/soc" , "ranges" , NULL, 0); |
81 | qemu_fdt_setprop_string(fdt, "/soc" , "compatible" , "simple-bus" ); |
82 | qemu_fdt_setprop_cell(fdt, "/soc" , "#size-cells" , 0x2); |
83 | qemu_fdt_setprop_cell(fdt, "/soc" , "#address-cells" , 0x2); |
84 | |
85 | nodename = g_strdup_printf("/memory@%lx" , |
86 | (long)memmap[SPIKE_DRAM].base); |
87 | qemu_fdt_add_subnode(fdt, nodename); |
88 | qemu_fdt_setprop_cells(fdt, nodename, "reg" , |
89 | memmap[SPIKE_DRAM].base >> 32, memmap[SPIKE_DRAM].base, |
90 | mem_size >> 32, mem_size); |
91 | qemu_fdt_setprop_string(fdt, nodename, "device_type" , "memory" ); |
92 | g_free(nodename); |
93 | |
94 | qemu_fdt_add_subnode(fdt, "/cpus" ); |
95 | qemu_fdt_setprop_cell(fdt, "/cpus" , "timebase-frequency" , |
96 | SIFIVE_CLINT_TIMEBASE_FREQ); |
97 | qemu_fdt_setprop_cell(fdt, "/cpus" , "#size-cells" , 0x0); |
98 | qemu_fdt_setprop_cell(fdt, "/cpus" , "#address-cells" , 0x1); |
99 | |
100 | for (cpu = s->soc.num_harts - 1; cpu >= 0; cpu--) { |
101 | nodename = g_strdup_printf("/cpus/cpu@%d" , cpu); |
102 | char *intc = g_strdup_printf("/cpus/cpu@%d/interrupt-controller" , cpu); |
103 | char *isa = riscv_isa_string(&s->soc.harts[cpu]); |
104 | qemu_fdt_add_subnode(fdt, nodename); |
105 | qemu_fdt_setprop_cell(fdt, nodename, "clock-frequency" , |
106 | SPIKE_CLOCK_FREQ); |
107 | qemu_fdt_setprop_string(fdt, nodename, "mmu-type" , "riscv,sv48" ); |
108 | qemu_fdt_setprop_string(fdt, nodename, "riscv,isa" , isa); |
109 | qemu_fdt_setprop_string(fdt, nodename, "compatible" , "riscv" ); |
110 | qemu_fdt_setprop_string(fdt, nodename, "status" , "okay" ); |
111 | qemu_fdt_setprop_cell(fdt, nodename, "reg" , cpu); |
112 | qemu_fdt_setprop_string(fdt, nodename, "device_type" , "cpu" ); |
113 | qemu_fdt_add_subnode(fdt, intc); |
114 | qemu_fdt_setprop_cell(fdt, intc, "phandle" , 1); |
115 | qemu_fdt_setprop_cell(fdt, intc, "linux,phandle" , 1); |
116 | qemu_fdt_setprop_string(fdt, intc, "compatible" , "riscv,cpu-intc" ); |
117 | qemu_fdt_setprop(fdt, intc, "interrupt-controller" , NULL, 0); |
118 | qemu_fdt_setprop_cell(fdt, intc, "#interrupt-cells" , 1); |
119 | g_free(isa); |
120 | g_free(intc); |
121 | g_free(nodename); |
122 | } |
123 | |
124 | cells = g_new0(uint32_t, s->soc.num_harts * 4); |
125 | for (cpu = 0; cpu < s->soc.num_harts; cpu++) { |
126 | nodename = |
127 | g_strdup_printf("/cpus/cpu@%d/interrupt-controller" , cpu); |
128 | uint32_t intc_phandle = qemu_fdt_get_phandle(fdt, nodename); |
129 | cells[cpu * 4 + 0] = cpu_to_be32(intc_phandle); |
130 | cells[cpu * 4 + 1] = cpu_to_be32(IRQ_M_SOFT); |
131 | cells[cpu * 4 + 2] = cpu_to_be32(intc_phandle); |
132 | cells[cpu * 4 + 3] = cpu_to_be32(IRQ_M_TIMER); |
133 | g_free(nodename); |
134 | } |
135 | nodename = g_strdup_printf("/soc/clint@%lx" , |
136 | (long)memmap[SPIKE_CLINT].base); |
137 | qemu_fdt_add_subnode(fdt, nodename); |
138 | qemu_fdt_setprop_string(fdt, nodename, "compatible" , "riscv,clint0" ); |
139 | qemu_fdt_setprop_cells(fdt, nodename, "reg" , |
140 | 0x0, memmap[SPIKE_CLINT].base, |
141 | 0x0, memmap[SPIKE_CLINT].size); |
142 | qemu_fdt_setprop(fdt, nodename, "interrupts-extended" , |
143 | cells, s->soc.num_harts * sizeof(uint32_t) * 4); |
144 | g_free(cells); |
145 | g_free(nodename); |
146 | |
147 | if (cmdline) { |
148 | qemu_fdt_add_subnode(fdt, "/chosen" ); |
149 | qemu_fdt_setprop_string(fdt, "/chosen" , "bootargs" , cmdline); |
150 | } |
151 | } |
152 | |
153 | static void spike_board_init(MachineState *machine) |
154 | { |
155 | const struct MemmapEntry *memmap = spike_memmap; |
156 | |
157 | SpikeState *s = g_new0(SpikeState, 1); |
158 | MemoryRegion *system_memory = get_system_memory(); |
159 | MemoryRegion *main_mem = g_new(MemoryRegion, 1); |
160 | MemoryRegion *mask_rom = g_new(MemoryRegion, 1); |
161 | int i; |
162 | unsigned int smp_cpus = machine->smp.cpus; |
163 | |
164 | /* Initialize SOC */ |
165 | object_initialize_child(OBJECT(machine), "soc" , &s->soc, sizeof(s->soc), |
166 | TYPE_RISCV_HART_ARRAY, &error_abort, NULL); |
167 | object_property_set_str(OBJECT(&s->soc), machine->cpu_type, "cpu-type" , |
168 | &error_abort); |
169 | object_property_set_int(OBJECT(&s->soc), smp_cpus, "num-harts" , |
170 | &error_abort); |
171 | object_property_set_bool(OBJECT(&s->soc), true, "realized" , |
172 | &error_abort); |
173 | |
174 | /* register system main memory (actual RAM) */ |
175 | memory_region_init_ram(main_mem, NULL, "riscv.spike.ram" , |
176 | machine->ram_size, &error_fatal); |
177 | memory_region_add_subregion(system_memory, memmap[SPIKE_DRAM].base, |
178 | main_mem); |
179 | |
180 | /* create device tree */ |
181 | create_fdt(s, memmap, machine->ram_size, machine->kernel_cmdline); |
182 | |
183 | /* boot rom */ |
184 | memory_region_init_rom(mask_rom, NULL, "riscv.spike.mrom" , |
185 | memmap[SPIKE_MROM].size, &error_fatal); |
186 | memory_region_add_subregion(system_memory, memmap[SPIKE_MROM].base, |
187 | mask_rom); |
188 | |
189 | if (machine->kernel_filename) { |
190 | riscv_load_kernel(machine->kernel_filename); |
191 | } |
192 | |
193 | /* reset vector */ |
194 | uint32_t reset_vec[8] = { |
195 | 0x00000297, /* 1: auipc t0, %pcrel_hi(dtb) */ |
196 | 0x02028593, /* addi a1, t0, %pcrel_lo(1b) */ |
197 | 0xf1402573, /* csrr a0, mhartid */ |
198 | #if defined(TARGET_RISCV32) |
199 | 0x0182a283, /* lw t0, 24(t0) */ |
200 | #elif defined(TARGET_RISCV64) |
201 | 0x0182b283, /* ld t0, 24(t0) */ |
202 | #endif |
203 | 0x00028067, /* jr t0 */ |
204 | 0x00000000, |
205 | memmap[SPIKE_DRAM].base, /* start: .dword DRAM_BASE */ |
206 | 0x00000000, |
207 | /* dtb: */ |
208 | }; |
209 | |
210 | /* copy in the reset vector in little_endian byte order */ |
211 | for (i = 0; i < sizeof(reset_vec) >> 2; i++) { |
212 | reset_vec[i] = cpu_to_le32(reset_vec[i]); |
213 | } |
214 | rom_add_blob_fixed_as("mrom.reset" , reset_vec, sizeof(reset_vec), |
215 | memmap[SPIKE_MROM].base, &address_space_memory); |
216 | |
217 | /* copy in the device tree */ |
218 | if (fdt_pack(s->fdt) || fdt_totalsize(s->fdt) > |
219 | memmap[SPIKE_MROM].size - sizeof(reset_vec)) { |
220 | error_report("not enough space to store device-tree" ); |
221 | exit(1); |
222 | } |
223 | qemu_fdt_dumpdtb(s->fdt, fdt_totalsize(s->fdt)); |
224 | rom_add_blob_fixed_as("mrom.fdt" , s->fdt, fdt_totalsize(s->fdt), |
225 | memmap[SPIKE_MROM].base + sizeof(reset_vec), |
226 | &address_space_memory); |
227 | |
228 | /* initialize HTIF using symbols found in load_kernel */ |
229 | htif_mm_init(system_memory, mask_rom, &s->soc.harts[0].env, serial_hd(0)); |
230 | |
231 | /* Core Local Interruptor (timer and IPI) */ |
232 | sifive_clint_create(memmap[SPIKE_CLINT].base, memmap[SPIKE_CLINT].size, |
233 | smp_cpus, SIFIVE_SIP_BASE, SIFIVE_TIMECMP_BASE, SIFIVE_TIME_BASE); |
234 | } |
235 | |
236 | static void spike_v1_10_0_board_init(MachineState *machine) |
237 | { |
238 | const struct MemmapEntry *memmap = spike_memmap; |
239 | |
240 | SpikeState *s = g_new0(SpikeState, 1); |
241 | MemoryRegion *system_memory = get_system_memory(); |
242 | MemoryRegion *main_mem = g_new(MemoryRegion, 1); |
243 | MemoryRegion *mask_rom = g_new(MemoryRegion, 1); |
244 | int i; |
245 | unsigned int smp_cpus = machine->smp.cpus; |
246 | |
247 | if (!qtest_enabled()) { |
248 | info_report("The Spike v1.10.0 machine has been deprecated. " |
249 | "Please use the generic spike machine and specify the ISA " |
250 | "versions using -cpu." ); |
251 | } |
252 | |
253 | /* Initialize SOC */ |
254 | object_initialize_child(OBJECT(machine), "soc" , &s->soc, sizeof(s->soc), |
255 | TYPE_RISCV_HART_ARRAY, &error_abort, NULL); |
256 | object_property_set_str(OBJECT(&s->soc), SPIKE_V1_10_0_CPU, "cpu-type" , |
257 | &error_abort); |
258 | object_property_set_int(OBJECT(&s->soc), smp_cpus, "num-harts" , |
259 | &error_abort); |
260 | object_property_set_bool(OBJECT(&s->soc), true, "realized" , |
261 | &error_abort); |
262 | |
263 | /* register system main memory (actual RAM) */ |
264 | memory_region_init_ram(main_mem, NULL, "riscv.spike.ram" , |
265 | machine->ram_size, &error_fatal); |
266 | memory_region_add_subregion(system_memory, memmap[SPIKE_DRAM].base, |
267 | main_mem); |
268 | |
269 | /* create device tree */ |
270 | create_fdt(s, memmap, machine->ram_size, machine->kernel_cmdline); |
271 | |
272 | /* boot rom */ |
273 | memory_region_init_rom(mask_rom, NULL, "riscv.spike.mrom" , |
274 | memmap[SPIKE_MROM].size, &error_fatal); |
275 | memory_region_add_subregion(system_memory, memmap[SPIKE_MROM].base, |
276 | mask_rom); |
277 | |
278 | if (machine->kernel_filename) { |
279 | riscv_load_kernel(machine->kernel_filename); |
280 | } |
281 | |
282 | /* reset vector */ |
283 | uint32_t reset_vec[8] = { |
284 | 0x00000297, /* 1: auipc t0, %pcrel_hi(dtb) */ |
285 | 0x02028593, /* addi a1, t0, %pcrel_lo(1b) */ |
286 | 0xf1402573, /* csrr a0, mhartid */ |
287 | #if defined(TARGET_RISCV32) |
288 | 0x0182a283, /* lw t0, 24(t0) */ |
289 | #elif defined(TARGET_RISCV64) |
290 | 0x0182b283, /* ld t0, 24(t0) */ |
291 | #endif |
292 | 0x00028067, /* jr t0 */ |
293 | 0x00000000, |
294 | memmap[SPIKE_DRAM].base, /* start: .dword DRAM_BASE */ |
295 | 0x00000000, |
296 | /* dtb: */ |
297 | }; |
298 | |
299 | /* copy in the reset vector in little_endian byte order */ |
300 | for (i = 0; i < sizeof(reset_vec) >> 2; i++) { |
301 | reset_vec[i] = cpu_to_le32(reset_vec[i]); |
302 | } |
303 | rom_add_blob_fixed_as("mrom.reset" , reset_vec, sizeof(reset_vec), |
304 | memmap[SPIKE_MROM].base, &address_space_memory); |
305 | |
306 | /* copy in the device tree */ |
307 | if (fdt_pack(s->fdt) || fdt_totalsize(s->fdt) > |
308 | memmap[SPIKE_MROM].size - sizeof(reset_vec)) { |
309 | error_report("not enough space to store device-tree" ); |
310 | exit(1); |
311 | } |
312 | qemu_fdt_dumpdtb(s->fdt, fdt_totalsize(s->fdt)); |
313 | rom_add_blob_fixed_as("mrom.fdt" , s->fdt, fdt_totalsize(s->fdt), |
314 | memmap[SPIKE_MROM].base + sizeof(reset_vec), |
315 | &address_space_memory); |
316 | |
317 | /* initialize HTIF using symbols found in load_kernel */ |
318 | htif_mm_init(system_memory, mask_rom, &s->soc.harts[0].env, serial_hd(0)); |
319 | |
320 | /* Core Local Interruptor (timer and IPI) */ |
321 | sifive_clint_create(memmap[SPIKE_CLINT].base, memmap[SPIKE_CLINT].size, |
322 | smp_cpus, SIFIVE_SIP_BASE, SIFIVE_TIMECMP_BASE, SIFIVE_TIME_BASE); |
323 | } |
324 | |
325 | static void spike_v1_09_1_board_init(MachineState *machine) |
326 | { |
327 | const struct MemmapEntry *memmap = spike_memmap; |
328 | |
329 | SpikeState *s = g_new0(SpikeState, 1); |
330 | MemoryRegion *system_memory = get_system_memory(); |
331 | MemoryRegion *main_mem = g_new(MemoryRegion, 1); |
332 | MemoryRegion *mask_rom = g_new(MemoryRegion, 1); |
333 | int i; |
334 | unsigned int smp_cpus = machine->smp.cpus; |
335 | |
336 | if (!qtest_enabled()) { |
337 | info_report("The Spike v1.09.1 machine has been deprecated. " |
338 | "Please use the generic spike machine and specify the ISA " |
339 | "versions using -cpu." ); |
340 | } |
341 | |
342 | /* Initialize SOC */ |
343 | object_initialize_child(OBJECT(machine), "soc" , &s->soc, sizeof(s->soc), |
344 | TYPE_RISCV_HART_ARRAY, &error_abort, NULL); |
345 | object_property_set_str(OBJECT(&s->soc), SPIKE_V1_09_1_CPU, "cpu-type" , |
346 | &error_abort); |
347 | object_property_set_int(OBJECT(&s->soc), smp_cpus, "num-harts" , |
348 | &error_abort); |
349 | object_property_set_bool(OBJECT(&s->soc), true, "realized" , |
350 | &error_abort); |
351 | |
352 | /* register system main memory (actual RAM) */ |
353 | memory_region_init_ram(main_mem, NULL, "riscv.spike.ram" , |
354 | machine->ram_size, &error_fatal); |
355 | memory_region_add_subregion(system_memory, memmap[SPIKE_DRAM].base, |
356 | main_mem); |
357 | |
358 | /* boot rom */ |
359 | memory_region_init_rom(mask_rom, NULL, "riscv.spike.mrom" , |
360 | memmap[SPIKE_MROM].size, &error_fatal); |
361 | memory_region_add_subregion(system_memory, memmap[SPIKE_MROM].base, |
362 | mask_rom); |
363 | |
364 | if (machine->kernel_filename) { |
365 | riscv_load_kernel(machine->kernel_filename); |
366 | } |
367 | |
368 | /* reset vector */ |
369 | uint32_t reset_vec[8] = { |
370 | 0x297 + memmap[SPIKE_DRAM].base - memmap[SPIKE_MROM].base, /* lui */ |
371 | 0x00028067, /* jump to DRAM_BASE */ |
372 | 0x00000000, /* reserved */ |
373 | memmap[SPIKE_MROM].base + sizeof(reset_vec), /* config string pointer */ |
374 | 0, 0, 0, 0 /* trap vector */ |
375 | }; |
376 | |
377 | /* part one of config string - before memory size specified */ |
378 | const char *config_string_tmpl = |
379 | "platform {\n" |
380 | " vendor ucb;\n" |
381 | " arch spike;\n" |
382 | "};\n" |
383 | "rtc {\n" |
384 | " addr 0x%" PRIx64 "x;\n" |
385 | "};\n" |
386 | "ram {\n" |
387 | " 0 {\n" |
388 | " addr 0x%" PRIx64 "x;\n" |
389 | " size 0x%" PRIx64 "x;\n" |
390 | " };\n" |
391 | "};\n" |
392 | "core {\n" |
393 | " 0" " {\n" |
394 | " " "0 {\n" |
395 | " isa %s;\n" |
396 | " timecmp 0x%" PRIx64 "x;\n" |
397 | " ipi 0x%" PRIx64 "x;\n" |
398 | " };\n" |
399 | " };\n" |
400 | "};\n" ; |
401 | |
402 | /* build config string with supplied memory size */ |
403 | char *isa = riscv_isa_string(&s->soc.harts[0]); |
404 | char *config_string = g_strdup_printf(config_string_tmpl, |
405 | (uint64_t)memmap[SPIKE_CLINT].base + SIFIVE_TIME_BASE, |
406 | (uint64_t)memmap[SPIKE_DRAM].base, |
407 | (uint64_t)ram_size, isa, |
408 | (uint64_t)memmap[SPIKE_CLINT].base + SIFIVE_TIMECMP_BASE, |
409 | (uint64_t)memmap[SPIKE_CLINT].base + SIFIVE_SIP_BASE); |
410 | g_free(isa); |
411 | size_t config_string_len = strlen(config_string); |
412 | |
413 | /* copy in the reset vector in little_endian byte order */ |
414 | for (i = 0; i < sizeof(reset_vec) >> 2; i++) { |
415 | reset_vec[i] = cpu_to_le32(reset_vec[i]); |
416 | } |
417 | rom_add_blob_fixed_as("mrom.reset" , reset_vec, sizeof(reset_vec), |
418 | memmap[SPIKE_MROM].base, &address_space_memory); |
419 | |
420 | /* copy in the config string */ |
421 | rom_add_blob_fixed_as("mrom.reset" , config_string, config_string_len, |
422 | memmap[SPIKE_MROM].base + sizeof(reset_vec), |
423 | &address_space_memory); |
424 | |
425 | /* initialize HTIF using symbols found in load_kernel */ |
426 | htif_mm_init(system_memory, mask_rom, &s->soc.harts[0].env, serial_hd(0)); |
427 | |
428 | /* Core Local Interruptor (timer and IPI) */ |
429 | sifive_clint_create(memmap[SPIKE_CLINT].base, memmap[SPIKE_CLINT].size, |
430 | smp_cpus, SIFIVE_SIP_BASE, SIFIVE_TIMECMP_BASE, SIFIVE_TIME_BASE); |
431 | |
432 | g_free(config_string); |
433 | } |
434 | |
435 | static void spike_v1_09_1_machine_init(MachineClass *mc) |
436 | { |
437 | mc->desc = "RISC-V Spike Board (Privileged ISA v1.9.1)" ; |
438 | mc->init = spike_v1_09_1_board_init; |
439 | mc->max_cpus = 1; |
440 | } |
441 | |
442 | static void spike_v1_10_0_machine_init(MachineClass *mc) |
443 | { |
444 | mc->desc = "RISC-V Spike Board (Privileged ISA v1.10)" ; |
445 | mc->init = spike_v1_10_0_board_init; |
446 | mc->max_cpus = 1; |
447 | } |
448 | |
449 | static void spike_machine_init(MachineClass *mc) |
450 | { |
451 | mc->desc = "RISC-V Spike Board" ; |
452 | mc->init = spike_board_init; |
453 | mc->max_cpus = 1; |
454 | mc->is_default = 1; |
455 | mc->default_cpu_type = SPIKE_V1_10_0_CPU; |
456 | } |
457 | |
458 | DEFINE_MACHINE("spike_v1.9.1" , spike_v1_09_1_machine_init) |
459 | DEFINE_MACHINE("spike_v1.10" , spike_v1_10_0_machine_init) |
460 | DEFINE_MACHINE("spike" , spike_machine_init) |
461 | |