当前位置: 首页 > 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

相关文章:

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

文章目录 一、前言背景二、QEMU的字符设备模拟三、USART的发送1. USART发送的QEMU字符设备模拟2. MMIO设计3. 中断连接4. 复位 三、代码验证1. 输出到serial控制台2. 输出到文件 一、前言背景 QEMU是一款开源的模拟器及虚拟机管理器。而QEMU内置支持了一些开发板&#xff0c;我…...

目标检测篇---faster R-CNN

目标检测系列文章 第一章 R-CNN 第二篇 Fast R-CNN 目录 目标检测系列文章&#x1f4c4; 论文标题&#x1f9e0; 论文逻辑梳理1. 引言部分梳理 (动机与思想) &#x1f4dd; 三句话总结&#x1f50d; 方法逻辑梳理&#x1f680; 关键创新点&#x1f517; 方法流程图关键疑问解答…...

【计算机视觉】CV实战项目- 深度解析FaceAI:一款全能的人脸检测与图像处理工具库

深度解析FaceAI&#xff1a;一款全能的人脸检测与图像处理工具库 项目概述核心功能与技术实现1. 人脸检测与识别2. 数字化妆与轮廓标识3. 性别与表情识别4. 高级图像处理 实战指南&#xff1a;项目运行与开发环境配置典型应用示例常见问题与解决方案 学术背景与相关研究项目扩展…...

Linux下编译opencv-4.10.0(静态链接库和动态链接库)

1. 安装依赖 在编译之前&#xff0c;确保系统中安装了必要的依赖工具和库。运行以下命令安装&#xff1a; sudo apt update sudo apt-get install build-essential sudo apt-get install cmake git libgtk2.0-dev pkg-config sudo apt-get install libavcodec-dev libavforma…...

OpenBMC:BmcWeb login创建session

OpenBMC:BmcWeb login认证-CSDN博客 完成用户的认证后,还需要为该用户创建session,从而为后续的rest api访问铺平道路 inline void handleLogin(const crow::Request& req,const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {...auto session =persisten…...

Java高频面试之并发编程-07

hello啊&#xff0c;各位观众姥爷们&#xff01;&#xff01;&#xff01;本baby今天来报道了&#xff01;哈哈哈哈哈嗝&#x1f436; 面试官&#xff1a;线程之间有哪些通信方式&#xff1f; 在 Java 多线程编程中&#xff0c;线程间通信&#xff08;Inter-Thread Communica…...

四、Python编程基础04

这里写目录标题 一、集合[了解]二、函数1. 函数定义与调用2. 函数返回值 三、模块和包1. 登录案例2.登录案例优化3. 随机数案例 四、异常五、web自动化测试1、selenium介绍2、环境搭建具体安装步骤请看 3、入门案例3.1 自动化测试步骤3.2 案例 一、集合[了解] 1, 集合 set, 表…...

专家系统的一般结构解析——基于《人工智能原理与方法》的深度拓展

不同的专家系统,其功能与结构都不尽相同,但一般都包括人机接口、推理机、知识库及其管理系统、数据库及其管理系统、知识获取机构、解释机构这六个部分,如下图所示。 一、人机接口(Human-Computer Interface, HCI) (一)基本思想与定义 1. 核心思想:双向交互的桥梁 人…...

手搓雷达图(MATLAB)

看下别人做出来什么效果 话不多说&#xff0c;咱们直接开始 %% 可修改 labels {用户等级, 发帖数, 发帖频率, 点度中心度, 中介中心度, 帖子类型计分, 被列为提案数}; cluster_centers [0.8, 4.5, 3.2, 4.0, 3.8, 4.5, 4.2; % 核心用户0.2, 0.5, 0.3, 0.2, 0.1, 0.0, 0.0;…...

FastMCP与FastAPI:构建自定义MCP服务器

FastMCP与FastAPI&#xff1a;构建自定义MCP服务器 模型上下文协议(Model Context Protocol, MCP)是一种让AI模型与外部工具和服务交互的标准。本文将介绍FastMCP和FastAPI&#xff0c;并通过实例展示如何创建自定义MCP服务器。 MCP基础概念 MCP允许语言模型: 访问外部工具…...

基于 springboot+vue+elementui 的办公自动化系统设计(

基于 springbootvueelementui 的办公自动化系统设计(前端代码在oa文件夹里) 系统主要功能&#xff1a; 1、统一的信息发布平台&#xff1a;公告栏&#xff0c;公司活动&#xff0c;项目进度&#xff0c;公司周报 2、统一的文件管理平台&#xff1a;收文管理、发文管理。文件查询…...

Java发展史及版本详细说明

Java发展史及版本详细说明 1. Java 1.0&#xff08;1996年1月23日&#xff09; 核心功能&#xff1a; 首个正式版本&#xff0c;支持面向对象编程、垃圾回收、网络编程。包含基础类库&#xff08;java.lang、java.io、java.awt&#xff09;。支持Applet&#xff08;浏览器嵌入…...

高企复审奖补!2025年合肥市高新技术企业重新认定奖励补贴政策及申报条件

一、合肥市高新技术企业重新认定奖励补贴政策 &#xff08;一&#xff09;高新区高新技术企业重新认定复审补贴奖励 重新认定为国家高新技术企业的给予5万元一次性奖励。 &#xff08;二&#xff09;经开区高新技术企业重新认定复审补贴奖励 对重新认定的企业&#xff0c;给…...

elementui日历显示红点及根据日程范围判断是否有红点

生成指定日期范围内的所有日期 generateDateRange(startStr, endStr) {const dates []; 日期列表const start new Date(startStr); 日程开始日期const end new Date(endStr); 日程结束日期end.setHours(23, 59, 59, 999); 结束的那一天设置为23点59分59秒// 生成日期范围内…...

第六章 QT基础:5、QT的UDP网络编程

在 Qt 中&#xff0c;QUdpSocket 类用于实现基于 UDP 协议的网络通信。UDP&#xff08;用户数据报协议&#xff09;是一种无连接的协议&#xff0c;与 TCP 不同&#xff0c;它不需要建立连接&#xff0c;因此它的传输速度较快&#xff0c;但也不保证数据的可靠传输。 1. Qt UDP…...

JAVA设计模式——(五)享元模式(Flyweight Pattern)

JAVA设计模式——&#xff08;五&#xff09;享元模式&#xff08;Flyweight Pattern&#xff09; 概念理解实现创建内部状态定义享元工厂测试 适用性 概念 使共享对象可以有效的支持大量的细粒度对象。主要采用池技术实现。 理解 享元模式主要解决大量对象的共享&#xff0…...

电竞俱乐部护航点单小程序,和平地铁俱乐部点单系统,三角洲护航小程序,暗区突围俱乐部小程序

电竞俱乐部护航点单小程序开发&#xff0c;和平地铁俱乐部点单系统&#xff0c;三角洲护航小程序&#xff0c;暗区突围俱乐部小程序开发 端口包含&#xff1a; 超管后台&#xff0c; 老板端&#xff0c;打手端&#xff0c;商家端&#xff0c;客服端&#xff0c;管事端&#x…...

mybatis log convert使用

1. idea 搜索插件 mybatis log convert. 安装后重启idea 启动程序. 日志打印后选中输出的内容右键。这里随意选中一段日志做演示 必须要在console中复制才能使用这个日志解析 2. 还有一种用法。没找到...

[U-Net]DA-TRANSUNET

论文题目:DA-TRANSUNET: INTEGRATING SPATIAL AND CHANNEL DUAL ATTENTION WITH TRANSFORMER U-NET FOR MEDICAL IMAGE SEGMENTATION ∗ 中文题目:DA-TRANSUNET:结合空间和通道双注意力与Transformer U-NET的医学图像分割算法 0摘要 精确的医学图像分割对疾病定量和治疗评…...

AI大模型从0到1记录学习 数据结构和算法 day20

4.3 分治算法 4.3.1 概述 分治算法的基本思想为&#xff1a;将原问题递归的分解为若干个&#xff08;通常是两个以上&#xff09;规模较小、相互独立且性质相同的子问题&#xff0c;直到子问题足够简单&#xff0c;简单到可以直接求解。然后再返回结果&#xff0c;逐个解决上层…...

面阵相机中M12镜头和远心镜头的区别及性能优势

以下是关于面阵相机中M12镜头和远心镜头的区别及性能优势的详细分析&#xff0c;结合知识库内容整理如下&#xff1a; 一、M12镜头与远心镜头的核心区别 1. 设计原理与光学特性 特性M12镜头远心镜头光学设计标准镜头设计&#xff0c;无特殊光学校正&#xff0c;依赖传统光路。…...

[创业之路-385]:企业法务 - 初创公司如何做好商业秘密的管理

一、商业秘密的定义与价值 定义&#xff1a; 商业秘密是企业的核心资产&#xff0c;包括技术信息&#xff08;如算法、配方&#xff09;和经营信息&#xff08;如客户名单、定价策略&#xff09;&#xff0c;具有非公开性、商业价值、保密性三大特征。价值&#xff1a; 初创公…...

Qt5.15.2+OpenCV4.9.0开发环境搭建详细图文教程(OpenCV使用Qt自带MinGW编译的全过程,包教包会)

【系列专栏】:博主结合工作实践输出的,解决实际问题的专栏,朋友们看过来! 《项目案例分享》 《极客DIY开源分享》 《嵌入式通用开发实战》 《C++语言开发基础总结》 《从0到1学习嵌入式Linux开发》 《QT开发实战》 《Android开发实战》 《实用硬件方案设计》 《结构建模设…...

怎么实现RAG检索相似文档排序:similarities

怎么实现RAG检索相似文档排序:similarities top_5_indices = similarities.argsort()[-5:][::-1] 这行代码的作用是从一组相似度得分中获取相似度最高的5个元素的索引。 similarities:这是一个包含了某个问题与所有文档之间余弦相似度得分的一维数组(假设 similarities 是通…...

NLP高频面试题(五十三)——深度学习正则化详解

一、动因篇 为什么要正则化? 深度神经网络通常包含数以万计甚至数以亿计的参数,模型容量极大,极易在有限的训练数据上“记住”噪声与异常样本,从而出现过拟合(overfitting)现象。过拟合导致模型在训练集上表现优异,但在测试集或真实场景中泛化能力大幅下降。正则化(r…...

2.idea查看某个类的方法名称和实现

2.idea查看某个类的方法名称和实现 ctrl鼠标左键单击类名 点击侧边栏Structure可看到方法名称的统计&#xff0c;这样直观...

FeignClient用法笔记

1. FeignClient简介 在微服务架构中总需要各个服务相互调用&#xff0c;各个服务又是以接口方式暴露&#xff0c;所以需要Http远程调用&#xff1b;为了简化调用&#xff0c;Spring Cloud OpenFeign 库提供了org.springframework.cloud.openfeign.FeignClient 注解&#xff1a…...

【Redis】 Redis中常见的数据类型(二)

文章目录 前言一、 List 列表1. List 列表简介2.命令3. 阻塞版本命令4. 内部编码5. 使用场景 二、Set 集合1. Set简单介绍2. 普通命令3 . 集合间操作4. 内部编码5. 使用场景 三、Zset 有序集合1.Zset 有序集合简介2. 普通命令3. 集合间操作4. 内部编码5. 使用场景 结语 前言 在…...

电力作业安全工器具全解析:分类、配置与检查要点

在电力行业&#xff0c;每一次作业都面临潜在危险&#xff0c;安全工器具是保障作业人员生命安全的关键。今天&#xff0c;金能电力带大家深入了解电力作业中常见的安全工器具&#xff0c;以及它们的检查与使用要点。 电力作业中安全工器具种类繁多。绝缘安全工器具因直接关联带…...

PowerBI-使用参数动态修改数据源路径

PowerBI-使用参数动态修改数据源路径 在PowerQuery中可以使用参数&#xff0c;通过参数我们可以将多个文件路径相同的字符串进行替换。 以一个案例分享下过程&#xff1a; 第一步&#xff0c;导入一个含有多个sheet表的EXCEL工作薄&#xff0c;点击转换数据&#xff0c;如图…...

Temperature

模型中Temperature参数的详细解释 ​​Temperature​​ 是生成模型&#xff08;如GPT、LLaMA等&#xff09;中用于控制输出多样性和随机性的关键超参数。它通过调整模型预测概率分布的平滑程度&#xff0c;直接影响生成文本的创造性与稳定性。 模型中Temperature参数的详细解…...

C++ 日志系统实战第二步:不定参数函数解析

全是通俗易懂的讲解&#xff0c;如果你本节之前的知识都掌握清楚&#xff0c;那就速速来看我的项目笔记吧~ 相关技术知识补充 不定参宏函数 在 C 语言中&#xff0c;不定参宏函数是一种强大的工具&#xff0c;它允许宏接受可变数量的参数&#xff0c;类似于不定参函数&#…...

【高并发】 MySQL锁优化策略

在数据库高并发场景中&#xff0c;行锁、表锁和高并发处理是密切相关的概念&#xff0c;它们共同影响着系统的并发性能和数据一致性。以下是三者的详细解释及高并发处理的策略&#xff1a; 1. 行锁&#xff08;Row-Level Locking&#xff09; 行锁是数据库中最小的锁粒度&…...

C语言——填充矩阵

C语言——填充矩阵 一、问题描述二、格式要求1.输入形式2.输出形式3.样例 三、实验代码 一、问题描述 编程实现自动填充nn矩阵元素数值&#xff0c;填充规则为&#xff1a;从第一行最后一列矩阵元素开始按逆时针方向螺旋式填充数值1&#xff0c;2&#xff0c;…&#xff0c;nn…...

CSS3 基础(背景-文本效果)

二、背景效果 属性功能示例值说明background设置背景颜色或渐变background: linear-gradient(45deg, #4CAF50, #FF5722);设置背景颜色、图片或渐变效果。background-size调整背景图片大小background-size: cover;设置背景图片的显示大小&#xff0c;如 cover 或 contain。back…...

点云配准算法之NDT算法原理详解

一、算法概述 NDT&#xff08;Normal Distributions Transform&#xff09;最初用于2D激光雷达地图构建&#xff08;Biber & Straer, 2003&#xff09;&#xff0c;后扩展为3D点云配准。它将点云数据空间划分为网格单元&#xff08;Voxel&#xff09;&#xff0c;在每个体…...

springboot在eclipse里面运行 run as 是Java Application还是 Maven

在 Eclipse 里运行 Spring Boot 项目时&#xff0c;既可以选择以“Java Application”方式运行&#xff0c;也可以通过 Maven 命令来运行&#xff0c;下面为你详细介绍这两种方式及适用场景。 以“Java Application”方式运行 操作步骤 在项目中找到带有 SpringBootApplicat…...

Redis 基础和高级用法入门

redis 是什么&#xff1f; Redis是一个远程内存数据库&#xff0c;它不仅性能强劲&#xff0c;而且还具有复制特性以及为解决问题而生的独一无二的数据模型。Redis提供了5种不同类型的数据结构&#xff0c;各式各样的问题都可以很自然地映射到这些数据结构上&#xff1a…...

使用vue2开发一个在线旅游预订平台-前端静态网站项目练习

hello,大家好&#xff0c;今天给大家再分享一个前端vue2练习项目-在线旅游预订平台。我们在学习编程的时候&#xff0c;除了学习编程的基础知识&#xff0c;为了让我们快速的掌握一门编程技术&#xff0c;肯定离不开各种项目的练习&#xff0c;今天分享的这个前端练习项目&…...

Ext Direct 功能与使用详解

Ext Direct 是 Ext JS 框架中的一个功能模块,旨在简化前端 JavaScript 应用与后端服务器之间的通信。其核心思想是通过远程过程调用(RPC)协议,将服务器端的方法透明地映射为前端可直接调用的 JavaScript 函数,从而减少手动编写 Ajax 请求和处理响应的代码量。 一、Ext Dir…...

Android移动应用开发入门示例:Activity跳转界面

介绍如何使用LinearLayout布局实现基本的UI设计&#xff0c;并实现两个Activity之间的跳转&#xff0c;适合刚接触Android Studio的新手学习。我们将使用Java语言开发&#xff0c;布局采用XML文件。以下为完整源码与运行说明&#xff1a; 案例前的准备工作&#xff1a; 1.1XM…...

【hadoop】HBase分布式数据库安装部署

一、HBase集群的安装与配置 步骤&#xff1a; 1、使用XFTP将HBase安装包hbase-1.2.0-bin.tar.gz发送到master机器的主目录。 2、解压安装包&#xff1a; tar -zxvf ~/hbase-1.2.0-bin.tar.gz 3、修改文件夹的名字&#xff0c;将其改为hbase&#xff0c;或者创建软连接也可…...

理解npm的工作原理:优化你的项目依赖管理流程

目录 什么是npm npm核心功能 npm 常用指令及其作用 执行npm i 发生了什么? 1. 解析命令与参数 2. 检查依赖文件 3. 依赖版本解析与树构建 4. 缓存检查与包下载 5. 解压包到 node_modules 6. 更新 package-lock.json 7. 处理特殊依赖类型 8. 执行生命周期脚本 9. …...

【Python笔记 04】输入函数、转义字符

一、Input 输入函数 prompt是提示&#xff0c;会在控制台显示&#xff0c;用作提示函数。 name input("请输入您的姓名&#xff1a;") print (name)提示你输入任意信息&#xff1a; 输入input test后回车&#xff0c;他输出input test 二、常用的转义字符 只讲…...

MySQL数据库基本操作-DQL-基本查询

数据库的操作中&#xff0c;查询是最重要的 一、基本查询-数据准备 -- 数据准备 create database if not exists mydb2; use mydb2; create table product( pid int primary key auto_increment, pname varchar(20) not null, price double, category_id varchar(20) …...

13、性能优化:魔法的流畅之道——React 19 memo/lazy

一、记忆封印术&#xff08;React.memo&#xff09; 1. 咒语本质 "memo是时间转换器的记忆晶石&#xff0c;冻结无意义的能量波动&#xff01;" 通过浅层比较&#xff08;shallowCompare&#xff09;或自定义预言契约&#xff0c;阻止组件在props未变时重新渲染。 …...

低代码平台开发手机USB-HID调试助手

项目介绍 USB-HID调试助手是一种专门用于调试和测试USB-HID设备的软件工具。USB-HID设备是一类通过USB接口与计算机通信的设备&#xff0c;常见的HID设备包括键盘、鼠标、游戏控制器、以及一些专用的工业控制设备等。 主要功能包括&#xff1a; 数据监控&#xff1a;实时监控和…...

Langchain_Agent+数据库

本处使用Agent数据库&#xff0c;可以直接执行SQL语句。可以多次循环查询问题 前文通过chain去联系数据库并进行操作&#xff1b; 通过链的不断内嵌组合&#xff0c;生成SQL在执行SQL再返回。 初始化 import os from operator import itemgetterimport bs4 from langchain.ch…...

Code Splitting 分包策略

以下是关于分包策略(Code Splitting)的深度技术解析,涵盖原理、策略、工具实现及优化技巧: 一、分包核心价值与底层原理 1. 核心价值矩阵 维度未分包场景合理分包后首屏速度需加载全部资源仅加载关键资源缓存效率任意修改导致全量缓存失效按模块变更频率分层缓存并行加载单…...

AI 开发入门之 RAG 技术

目录 一、从一个简单的问题开始二、语言模型“闭卷考试”的困境三、RAG 是什么—LLM 的现实世界“外挂”四、RAG 的七步流程第一步&#xff1a;加载数据&#xff08;Load&#xff09;第二步&#xff1a;切分文本&#xff08;Chunking&#xff09;第三步&#xff1a;向量化&…...