当前位置: 首页 > news >正文

从零开始在Win上添加一块QEMU开发板(四)实现简单USART

文章目录

  • 一、前言背景
  • 二、QEMU的字符设备模拟
  • 三、USART的发送
    • 1. USART发送的QEMU字符设备模拟
    • 2. MMIO设计
    • 3. 中断连接
    • 4. 复位
  • 三、代码验证
    • 1. 输出到serial控制台
    • 2. 输出到文件

一、前言背景

QEMU是一款开源的模拟器及虚拟机管理器。而QEMU内置支持了一些开发板,我们可以基于这些内置的板子来做操作系统等软件的配置。但是实际市面上很多板子QEMU中是没有提供支持的,这需要我们根据QEMU的源码自定义一些开发板,然后再重新编译,以满足灵活的需求。
这是基于自己的理解写的一份学习笔记,方便我日后查阅,也给新来的朋友给予一些帮助。

从零开始在Win上添加一块QEMU开发板(三)让板子跑起来 中我们实现了CPU的继承和内存的虚拟化,让板子已经可以运行一些代码了。本篇将继续实践阶段,目标是基于自定义的 ricky 开发板模型,完成 STM32 USART 模块的模拟,并在 QEMU 中运行一个简单的测试程序,通过串口打印输出 “HelloWorld”。

二、QEMU的字符设备模拟

UART 的收发过程涉及数据交互,因此在模拟过程中,我们需要实现 QEMU 模型中相应的数据传输功能。
QEMU 提供了 CharBackend 结构体来完成字符设备的模拟,它是连接前端设备与后端设备的核心结构。

常见的数据发送接口包括 qemu_chr_fe_write()qemu_chr_fe_write_all(),它们用于从设备前端向后端发送数据。对于接收数据的处理,QEMU 提供了回调机制,可以通过注册 IOCanReadHandler *fd_can_readIOReadHandler *fd_read 回调来接收来自后端的数据并传给设备模拟器。

CharBackend 可绑定多种类型的后端设备(Chardev *chr),包括但不限于:

  • 主机终端(如 stdio 或 serial)
  • 网络(如 tcp 或 udp)
  • 文件
  • 虚拟设备(如 null 或 mux)

这是CharBackend结构体的定义:

struct CharBackend {Chardev *chr;IOEventHandler *chr_event;IOCanReadHandler *chr_can_read;IOReadHandler *chr_read;BackendChangeHandler *chr_be_change;void *opaque;unsigned int tag;bool fe_is_open;
};

我们可以使用 system/system.h 中的 serial_hd(x) 接口(这是一个 Chardev )来使用QEMU命令行上的serial窗口输出和接收数据。

我们在类型属性结构体 RickySocUsartState 中创建一个 CharBackend 对象:

struct RickySocUsartState {/* <private> */SysBusDevice parent_obj;/* <public> */CharBackend chr;
};

并通过属性 "chardev" 传入到 CharBackendChardev *chr 对象:

static const Property ricky_soc_usart_properties[] = {DEFINE_PROP_CHR("chardev", RickySocUsartState, chr),
};static void ricky_soc_usart_class_init(ObjectClass *klass, void *data)
{DeviceClass *dc = DEVICE_CLASS(klass);device_class_set_props(dc, ricky_soc_usart_properties);
}

并在SoC中创建USART对象并传入 "chardev" 对象为 serial_hd(serial_id)

void ricky_soc_usart_create(RickySocUsartState *usart, int serial_id, qemu_irq irq, hwaddr base, Error **errp)
{DeviceState *dev;SysBusDevice *busdev;dev = DEVICE(usart);qdev_prop_set_chr(dev, "chardev", serial_hd(serial_id));if (!sysbus_realize(SYS_BUS_DEVICE(usart), errp)) {return;}
}

并绑定 s->chr 的接收回调函数

static int ricky_soc_usart_can_receive(void *opaque)
{RickySocUsartState *s = RICKY_SOC_USART(opaque);if (!(s->usart_sr & USART_SR_RXNE)) {return 1;}return 0;
}static void ricky_soc_usart_receive(void *opaque, const uint8_t *buf, int size)
{//   RickySocUsartState *s = RICKY_SOC_USART(opaque);//    DeviceState *d = DEVICE(s);}
static void ricky_soc_usart_realize(DeviceState *dev, Error **errp)
{RickySocUsartState *s = RICKY_SOC_USART(dev);qemu_chr_fe_set_handlers(&s->chr, ricky_soc_usart_can_receive,ricky_soc_usart_receive, NULL, NULL,s, NULL, true);
}

此处我们暂未实现 ricky_soc_usart_receive(),仅通过 ricky_soc_usart_can_receive() 判断是否可以接收数据。根据手册,当一个字符被接收并转移至接收寄存器时,状态寄存器 SR 的 RXNE 位将被置位,表示数据可以被读取。
rev

三、USART的发送

1. USART发送的QEMU字符设备模拟

在STM32F1x 的 Reference Manual 中可以看到USART章节:

USART introduction
The universal synchronous asynchronous receiver transmitter (USART) offers a flexible
means of full-duplex data exchange with external equipment requiring an industry standard
NRZ asynchronous serial data format. The USART offers a very wide range of baud rates
using a fractional baud rate generator.
It supports synchronous one-way communication and half-duplex single wire
communication. It also supports the LIN (local interconnection network), Smartcard Protocol
and IrDA (infrared data association) SIR ENDEC specifications, and modem operations
(CTS/RTS). It allows multiprocessor communication.
High speed data communication is possible by using the DMA for multibuffer configuration

可以看到USART具有很多功能,在本次模拟中,我们聚焦其最基本的异步串口发送功能。
下面节选了一部分手册内容:
Ref1
Ref2
Ref3
通过手册我们可以知道,USART的数据发送的步骤是:

  1. 通过在 USART_CR1 寄存器上置位 UE 位来激活USART
  2. 编程 USART_CR1M 位来定义字长。
  3. USART_CR2 中编程停止位的位数。
  4. 如果采用多缓冲器通信,配置 USART_CR3 中的DMA使能位(DMAT)。按多缓冲器通信中
    的描述配置DMA寄存器。
  5. 利用 USART_BRR 寄存器选择要求的波特率。
  6. 设置 USART_CR1 中的 TE 位,发送一个空闲帧作为第一次数据发送。
  7. 把要发送的数据写进 USART_DR 寄存器(此动作清除 TXE 位)。在只有一个缓冲器的情况
    下,对每个待发送的数据重复步骤7。
  8. USART_DR 寄存器中写入最后一个数据字后,要等待 TC=1,它表示最后一个数据帧的
    传输结束。当需要关闭USART或需要进入停机模式之前,需要确认传输结束,避免破坏
    最后一次传输。

但是我们本次使用的是QEMU的字符设备模拟进行模拟USART的数据输出,所以我们的数据不存在时序,只需要确定大小端,以及在合适的地方执行:

	qemu_chr_fe_write_all(&s->chr, &ch, 1);

而清零 TXE 位是通过对数据寄存器的写操作来完成的。TXE 位由硬件来设置,它表明:
● 数据已经从 TDR 移送到移位寄存器,数据发送已经开始
TDR 寄存器被清空
● 下一个数据可以被写进 USART_DR 寄存器而不会覆盖先前的数据

而QEMU的字符设备模拟时,数据发送是同步的,也就说数据写进 USART_DR 寄存器是就会立即执行 qemu_chr_fe_write_all(&s->chr, &ch, 1);,执行完之后也就立即置位了,也就是操作是:

	ch = value;s->usart_sr &= ~(SART_SR_TXE);	// 硬件清零TXE位qemu_chr_fe_write_all(&s->chr, &ch, 1);s->usart_sr |= USART_SR_TXE;	// 硬件置位TXE位

TXE 位的置位与清除通常由硬件自动完成。在 QEMU 模拟中,若无并发问题,整个发送过程可以简化为:

	ch = value;qemu_chr_fe_write_all(&s->chr, &ch, 1);

2. MMIO设计

USART 是通过 MMIO(Memory-Mapped I/O) 实现与 CPU 的交互,因此需要完成 MMIO 区域的初始化,并将 MMIO 的读写操作转发到对应的回调函数:
读函数uint64_t (*read)(void *opaque, hwaddr addr, unsigned size)
写函数 void (*write)(void *opaque, hwaddr addr, uint64_t data, unsigned size)

在 QEMU 的字符设备模拟中,由于不涉及实际时序,我们可以省略字长、波特率、停止位等参数的模拟。这些通常只在真实硬件通信中起作用。

以下为 USART 所使用的一部分寄存器信息:
sr1
sr2
dr
MMIO 初始化

static void ricky_soc_usart_init(Object *obj)
{RickySocUsartState *s = RICKY_SOC_USART(obj);memory_region_init_io(&s->mmio, obj, &ricky_soc_usart_ops, s,TYPE_RICKY_SOC_USART, 0x400);sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
}

当然也得在Soc中创建USART对象时映射基地址(这里通过形参 base 传入):

void ricky_soc_usart_create(RickySocUsartState *usart, int serial_id, qemu_irq irq, hwaddr base, Error **errp)
{DeviceState *dev;SysBusDevice *busdev;dev = DEVICE(usart);qdev_prop_set_chr(dev, "chardev", serial_hd(serial_id));if (!sysbus_realize(SYS_BUS_DEVICE(usart), errp)) {return;}busdev = SYS_BUS_DEVICE(dev);sysbus_mmio_map(busdev, 0, base);
}

USART 的读写回调函数如下所示,具体根据手册中的寄存器映射来实现逻辑:

static void ricky_soc_usart_write(void *opaque, hwaddr addr,uint64_t val64, unsigned int size)
{RickySocUsartState *s = RICKY_SOC_USART(opaque);uint32_t value = val64;unsigned char ch;switch (addr) {case USART_SR:if (value <= 0x3FF) {/* I/O being synchronous, TXE is always set. In addition, it mayonly be set by hardware, so keep it set here. */s->usart_sr = value | USART_SR_TXE;} else {s->usart_sr &= value;}ricky_soc_usart_update_irq(s);return;case USART_DR:if (value < 0xF000) {ch = value;/* XXX this blocks entire thread. Rewrite to use* qemu_chr_fe_write and background I/O callbacks */qemu_chr_fe_write_all(&s->chr, &ch, 1);/* XXX I/O are currently synchronous, making it impossible forsoftware to observe transient states where TXE or TC aren'tset. Unlike TXE however, which is read-only, software mayclear TC by writing 0 to the SR register, so set it againon each write. */s->usart_sr |= USART_SR_TC;ricky_soc_usart_update_irq(s);}return;case USART_BRR:s->usart_brr = value;return;case USART_CR1:s->usart_cr1 = value;ricky_soc_usart_update_irq(s);return;case USART_CR2:s->usart_cr2 = value;return;case USART_CR3:s->usart_cr3 = value;return;case USART_GTPR:s->usart_gtpr = value;return;default:qemu_log_mask(LOG_GUEST_ERROR,"%s: Bad offset 0x%"HWADDR_PRIx"\n", __func__, addr);}
}static uint64_t ricky_soc_usart_read(void *opaque, hwaddr addr,unsigned int size)
{RickySocUsartState *s = RICKY_SOC_USART(opaque);uint64_t retvalue = 0;switch (addr) {case USART_SR:retvalue = s->usart_sr;qemu_chr_fe_accept_input(&s->chr);break;case USART_DR:retvalue = s->usart_dr & 0x3FF;s->usart_sr &= ~USART_SR_RXNE;qemu_chr_fe_accept_input(&s->chr);ricky_soc_usart_update_irq(s);break;case USART_BRR:retvalue = s->usart_brr;break;case USART_CR1:retvalue = s->usart_cr1;break;case USART_CR2:retvalue = s->usart_cr2;break;case USART_CR3:retvalue = s->usart_cr3;break;case USART_GTPR:retvalue = s->usart_gtpr;break;default:qemu_log_mask(LOG_GUEST_ERROR,"%s: Bad offset 0x%"HWADDR_PRIx"\n", __func__, addr);return 0;}return retvalue;
}static const MemoryRegionOps ricky_soc_usart_ops = {.read = ricky_soc_usart_read,.write = ricky_soc_usart_write,.endianness = DEVICE_NATIVE_ENDIAN,
};

MMIO 的实现应以手册为参考依据,必要时可根据模拟器需求进行简化或优化。
我们在可能会触发中断的位置留下函数 ricky_soc_usart_update_irq(),后续可以通过在该函数加入条件判断,来判断是否触发中断。

3. 中断连接

中断的实现通过 sysbus_init_irq() 初始化并挂入 SYS_BUS_DEVICE 中(这里存在QEMU的GPIO设计,大至是当我们挂入一个 SYS_BUS_DEVICE 设备中,每次挂入都会获得一个编号,从0开始取号):

static void ricky_soc_usart_init(Object *obj)
{RickySocUsartState *s = RICKY_SOC_USART(obj);sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);memory_region_init_io(&s->mmio, obj, &ricky_soc_usart_ops, s,TYPE_RICKY_SOC_USART, 0x400);sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
}

并在SoC中将中断连接形参 irq上(这里 sysbus_connect_irq() 的第二个形参 n 就是通过前面说的取号得到的数字来索引相应的 qemu_irq 的):

void ricky_soc_usart_create(RickySocUsartState *usart, int serial_id, qemu_irq irq, hwaddr base, Error **errp)
{DeviceState *dev;SysBusDevice *busdev;dev = DEVICE(usart);qdev_prop_set_chr(dev, "chardev", serial_hd(serial_id));if (!sysbus_realize(SYS_BUS_DEVICE(usart), errp)) {return;}busdev = SYS_BUS_DEVICE(dev);sysbus_mmio_map(busdev, 0, base);sysbus_connect_irq(busdev, 0, irq);
}

参入的形参 irq 与NVIC的中断号息息相关:

static const int usart_irq[RICKY_SOC_USART_NUM] = {37, 38, 39};
/* USART */for (i = 0; i < RICKY_SOC_USART_NUM; i++) {ricky_soc_usart_create(&(s->usart[i]), i, qdev_get_gpio_in(armv7m, usart_irq[i]), usart_addr[i], errp);}

intc
它对应着USART的全局中断服务函数。

我们目前仅支持 TXETCRXNE 三类中断事件,你可以按照需求添加更多:

static void ricky_soc_usart_update_irq(RickySocUsartState *s)
{uint32_t mask = s->usart_sr & s->usart_cr1;if (mask & (USART_SR_TXE | USART_SR_TC | USART_SR_RXNE)) {qemu_set_irq(s->irq, 1);} else {qemu_set_irq(s->irq, 0);}
}

4. 复位

根据各寄存器的手册复位值编写设备复位函数:

static void ricky_soc_usart_reset(DeviceState *dev)
{RickySocUsartState *s = RICKY_SOC_USART(dev);s->usart_sr = USART_SR_RESET;s->usart_dr = 0x00000000;s->usart_brr = 0x00000000;s->usart_cr1 = 0x00000000;s->usart_cr2 = 0x00000000;s->usart_cr3 = 0x00000000;s->usart_gtpr = 0x00000000;ricky_soc_usart_update_irq(s);
}

reg
最后,在 class 初始化中绑定复位函数:

static void ricky_soc_usart_class_init(ObjectClass *klass, void *data)
{DeviceClass *dc = DEVICE_CLASS(klass);device_class_set_legacy_reset(dc, ricky_soc_usart_reset); // RESETdevice_class_set_props(dc, ricky_soc_usart_properties);dc->realize = ricky_soc_usart_realize;
}

三、代码验证

1. 输出到serial控制台

使用STM32CubeMX生成代码,由于没有没有完成GPIO和RCC的模拟,我注释掉了相应的生成代码:

#include "main.h"
#include "usart.h"
#include "gpio.h"
void SystemClock_Config(void);int main(void)
{HAL_Init();// SystemClock_Config();// MX_GPIO_Init();MX_USART1_UART_Init();const uint8_t str[] = "Hello World!\r\n";HAL_UART_Transmit(&huart1, str, strlen((const char *)str), 0xFFFF);while (1){}}

编译得到ELF文件后,在我们刚刚制作的QEMU板中验证:
RUN1
RUN2
至此,成功在QEMU的字符模拟设备中输出 "Hello World!\n"

2. 输出到文件

当然,我们前面提到的QEMU的字符模拟设备,可以不只是 serial,还可以是文件、Tcp等等。
这里我们用输出到文件举个例子,
可以直接通过指令 -chardev 创建一个 file 字符设备,并通过 id=uart0 重定向 -serial

	${ROOT}/build/qemu-system-ricky.exe \-M RickyBoard \-kernel ${DEMO_PATH} \-monitor stdio \-d in_asm \-chardev file,id=uart0,path=uart_output.log \-serial chardev:uart0 

这样就可以在文件 uart_output.log 输出 "Hello World!\n"了。

但如果想在代码里操作呢?
这里我们也是一样再创建一个 Chardev *chardev

void ricky_soc_usart_create(RickySocUsartState *usart, int serial_id, qemu_irq irq, hwaddr base, Error **errp)
{DeviceState *dev;SysBusDevice *busdev;char filename[64];snprintf(filename, sizeof(filename), "file:output/uart%d.log", serial_id);char chardevname[64];snprintf(chardevname, sizeof(chardevname), "ricky_usart_chr%d", serial_id);Chardev *chardev = qemu_chr_new(chardevname, filename, NULL);dev = DEVICE(usart);// qdev_prop_set_chr(dev, "chardev", serial_hd(serial_id));qdev_prop_set_chr(dev, "chardev", chardev);if (!sysbus_realize(SYS_BUS_DEVICE(usart), errp)) {return;}busdev = SYS_BUS_DEVICE(dev);sysbus_mmio_map(busdev, 0, base);sysbus_connect_irq(busdev, 0, irq);
}

传入这个新的 chardev 作为QEMU的字符模拟设备,这里传入的 "file" 的字符设备(label 中需要包含字符设备的类型),它会在 qemu_chr_parse_compat() 中设置 filename 为文件路径:

Chardev *qemu_chr_new(const char *label, const char *filename,GMainContext *context)
{return qemu_chr_new_permit_mux_mon(label, filename, false, context);
}
static Chardev *qemu_chr_new_permit_mux_mon(const char *label,const char *filename,bool permit_mux_mon,GMainContext *context)
{return qemu_chr_new_from_name(label, filename, permit_mux_mon, context,true);
}
static Chardev *qemu_chr_new_from_name(const char *label, const char *filename,bool permit_mux_mon,GMainContext *context, bool replay)
{const char *p;Chardev *chr;QemuOpts *opts;Error *err = NULL;if (strstart(filename, "chardev:", &p)) {chr = qemu_chr_find(p);if (replay && chr) {qemu_chardev_set_replay(chr, &err);if (err) {error_report_err(err);return NULL;}}return chr;}opts = qemu_chr_parse_compat(label, filename, permit_mux_mon);if (!opts)return NULL;chr = do_qemu_chr_new_from_opts(opts, context, replay, &err);if (!chr) {error_report_err(err);goto out;}if (qemu_opt_get_bool(opts, "mux", 0)) {assert(permit_mux_mon);monitor_init_hmp(chr, true, &err);if (err) {error_report_err(err);object_unparent(OBJECT(chr));chr = NULL;goto out;}}out:qemu_opts_del(opts);return chr;
}
QemuOpts *qemu_chr_parse_compat(const char *label, const char *filename,bool permit_mux_mon)
{char host[65], port[33], width[8], height[8];int pos;const char *p;QemuOpts *opts;Error *local_err = NULL;opts = qemu_opts_create(qemu_find_opts("chardev"), label, 1, &local_err);if (local_err) {error_report_err(local_err);return NULL;}if (strstart(filename, "mon:", &p)) {if (!permit_mux_mon) {error_report("mon: isn't supported in this context");return NULL;}filename = p;qemu_opt_set(opts, "mux", "on", &error_abort);if (strcmp(filename, "stdio") == 0) {/* Monitor is muxed to stdio: do not exit on Ctrl+C by default* but pass it to the guest.  Handle this only for compat syntax,* for -chardev syntax we have special option for this.* This is what -nographic did, redirecting+muxing serial+monitor* to stdio causing Ctrl+C to be passed to guest. */qemu_opt_set(opts, "signal", "off", &error_abort);}}if (strcmp(filename, "null")    == 0 ||strcmp(filename, "pty")     == 0 ||strcmp(filename, "msmouse") == 0 ||strcmp(filename, "wctablet") == 0 ||strcmp(filename, "braille") == 0 ||strcmp(filename, "testdev") == 0 ||strcmp(filename, "stdio")   == 0) {qemu_opt_set(opts, "backend", filename, &error_abort);return opts;}if (strstart(filename, "vc", &p)) {qemu_opt_set(opts, "backend", "vc", &error_abort);if (*p == ':') {if (sscanf(p+1, "%7[0-9]x%7[0-9]", width, height) == 2) {/* pixels */qemu_opt_set(opts, "width", width, &error_abort);qemu_opt_set(opts, "height", height, &error_abort);} else if (sscanf(p+1, "%7[0-9]Cx%7[0-9]C", width, height) == 2) {/* chars */qemu_opt_set(opts, "cols", width, &error_abort);qemu_opt_set(opts, "rows", height, &error_abort);} else {goto fail;}}return opts;}if (strcmp(filename, "con:") == 0) {qemu_opt_set(opts, "backend", "console", &error_abort);return opts;}if (strstart(filename, "COM", NULL)) {qemu_opt_set(opts, "backend", "serial", &error_abort);qemu_opt_set(opts, "path", filename, &error_abort);return opts;}if (strstart(filename, "file:", &p)) {qemu_opt_set(opts, "backend", "file", &error_abort);qemu_opt_set(opts, "path", p, &error_abort);return opts;}if (strstart(filename, "pipe:", &p)) {qemu_opt_set(opts, "backend", "pipe", &error_abort);qemu_opt_set(opts, "path", p, &error_abort);return opts;}if (strstart(filename, "pty:", &p)) {qemu_opt_set(opts, "backend", "pty", &error_abort);qemu_opt_set(opts, "path", p, &error_abort);return opts;}if (strstart(filename, "tcp:", &p) ||strstart(filename, "telnet:", &p) ||strstart(filename, "tn3270:", &p) ||strstart(filename, "websocket:", &p)) {if (sscanf(p, "%64[^:]:%32[^,]%n", host, port, &pos) < 2) {host[0] = 0;if (sscanf(p, ":%32[^,]%n", port, &pos) < 1)goto fail;}qemu_opt_set(opts, "backend", "socket", &error_abort);qemu_opt_set(opts, "host", host, &error_abort);qemu_opt_set(opts, "port", port, &error_abort);if (p[pos] == ',') {if (!qemu_opts_do_parse(opts, p + pos + 1, NULL, &local_err)) {error_report_err(local_err);goto fail;}}if (strstart(filename, "telnet:", &p)) {qemu_opt_set(opts, "telnet", "on", &error_abort);} else if (strstart(filename, "tn3270:", &p)) {qemu_opt_set(opts, "tn3270", "on", &error_abort);} else if (strstart(filename, "websocket:", &p)) {qemu_opt_set(opts, "websocket", "on", &error_abort);}return opts;}if (strstart(filename, "udp:", &p)) {qemu_opt_set(opts, "backend", "udp", &error_abort);if (sscanf(p, "%64[^:]:%32[^@,]%n", host, port, &pos) < 2) {host[0] = 0;if (sscanf(p, ":%32[^@,]%n", port, &pos) < 1) {goto fail;}}qemu_opt_set(opts, "host", host, &error_abort);qemu_opt_set(opts, "port", port, &error_abort);if (p[pos] == '@') {p += pos + 1;if (sscanf(p, "%64[^:]:%32[^,]%n", host, port, &pos) < 2) {host[0] = 0;if (sscanf(p, ":%32[^,]%n", port, &pos) < 1) {goto fail;}}qemu_opt_set(opts, "localaddr", host, &error_abort);qemu_opt_set(opts, "localport", port, &error_abort);}return opts;}if (strstart(filename, "unix:", &p)) {qemu_opt_set(opts, "backend", "socket", &error_abort);if (!qemu_opts_do_parse(opts, p, "path", &local_err)) {error_report_err(local_err);goto fail;}return opts;}if (strstart(filename, "/dev/parport", NULL) ||strstart(filename, "/dev/ppi", NULL)) {qemu_opt_set(opts, "backend", "parallel", &error_abort);qemu_opt_set(opts, "path", filename, &error_abort);return opts;}if (strstart(filename, "/dev/", NULL)) {qemu_opt_set(opts, "backend", "serial", &error_abort);qemu_opt_set(opts, "path", filename, &error_abort);return opts;}error_report("'%s' is not a valid char driver", filename);fail:qemu_opts_del(opts);return NULL;
}

我们运行程序,最后发现已经打印在了 output/uart0.log
file

相关文章:

  • AD16如何对同值元件进行排序
  • 2024武汉邀请赛B.Countless Me
  • 【Java 数据结构】泛型
  • 【银河麒麟高级服务器操作系统】磁盘只读问题分析
  • X0405-ASEMI电源AI器件专用X0405
  • C#中实现JSON解析器
  • SSH 反向隧道访问内网服务
  • 【网络】TCP/IP协议学习
  • 【蓝桥杯】可分解的正整数
  • Linux学习笔记之动静态库
  • Java基础 — 运算符与输入器
  • css3新特性第七章(3D变换)
  • OpenBMC:BmcWeb login认证
  • vscode插件系列-2、认识vscode
  • Golang 闭包学习
  • 数论知识啊
  • 电子处方模块开发避坑指南:从互联网医院系统源码实践出发
  • 办公人导航网站
  • JavaWeb:HtmlCss
  • Python爬虫(3)HTML核心技巧:从零掌握class与id选择器,精准定位网页元素
  • 安徽铁塔回应“指挥调度中心大屏现不雅视频”:将严肃处理
  • 又双叒叕出差太空了!神二十成功出发,神十九乘组扫榻以待
  • 2024年度全国十大考古新发现公布,武王墩一号墓等入选
  • 联手华为猛攻主流市场,上汽集团总裁:上汽不做生态孤岛
  • 爱奇艺要转型做微剧?龚宇:是误解,微剧是增量业务,要提高投资回报效益
  • 言短意长|大学校门到底应不应该开放?