QEMU仿真中串口的输入和输出

最近在QEMU上搭建CM33的仿真环境,目前QEMU支持的CM33的设备是musca-b1等系列板卡,这个系列是ARM公司做的一个CM33的参考设计,也有相应的硬件开发板。因为我们主要就是关心仿真环境,所以只是关心系统的memory map和串口相关的资源.实际上对于MUSCA开发板, QEMU只是支持NVIC,串口和Systick这几个模块,其它诸如GPIO,timer等都是不支持的。

尽管QEMU支持的外设并不多,实际对于一般的软件层面的仿真已经足够了,这个实际也是QEMU的设计初衷,它的主要应用也是针对于软件的仿真,比如运行常见的操作系统,对于系统porting来说,实际就是中断和systick可以正常工作就可以了。

我们这里实际并没有引入操作系统,而是直接开发了一个bare metal的工程,实际也想试试QEMU对于TrustZone的支持,但是因为没能找到QEMU直接load多个elf的方法,或者合并elf的方法,暂时没能实现两个程序的同时下载,也就没能测试QEMU对于TrustZone是否支持。

QEMU对于外设的模拟和真实的硬件还是有一定区别的,比如模拟的串口并不需要进行开时钟,设置波特率等操作,需要发送数据就直接往DATA寄存器中写数据就可以了。并且是直接写直接发,单周期搞定,是不是很爽?所以对于模拟起来说,串口的输入输出可以直接通过下面的函数实现:

#define UART_TX_DATA_REG        (*(volatile uint32_t *)0x40105000)
#define UART_FIFO_REG           (*(volatile uint32_t *)0x40105018)
#define UART_FIFO_EMPTY_MASK    (1 << 4U)

void env_put_char(char c)
{
    UART_TX_DATA_REG = c;
}

char env_get_char()
{
    while (UART_FIFO_REG & UART_FIFO_EMPTY_MASK)
    {
    }
    return UART_TX_DATA_REG;
}

似乎还挺简单,跑个demo试试,输出完全没啥问题,但是输入嘛,似乎并不正常。比如我们调用一个inchar函数,键盘敲了大半天也没反应,后面debug进去看看,奇了怪了,我只需要一个char,怎么到了函数里面就成了1024个?

原来这个是因为我使用的nano libc里面的输入和输出都是自带缓存的,所以我们从串口输入的时候实际上是系统从stdin读取字符串,libc中这些外设都是模拟成一个文件的,所以读取的时候也就有缓存,比如默认的1K大小的缓存,所以我们调用inchar()的时候,因为有缓存,默认的读取长度就变成了1K。为了修正这个问题,我们需要禁用stdinstdout上的缓存,这个操作可以通过如下的方式实现:

setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);

里面的参数_IONBF代表不使用缓存,后面的0是缓存大小,setvbuf的原型如下:

int setvbuf(FILE *stream, char *buffer, int mode, size_t size)

各个参数的定义:

  • stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流。
  • buffer -- 这是分配给用户的缓冲。如果设置为 NULL,该函数会自动分配一个指定大小的缓冲。
  • mode -- 这指定了文件缓冲的模式:
    |模式|含义|
_IOFBF全缓冲:对于输出,数据在缓冲填满时被一次性写入。对于输入,缓冲会在请求输入且缓冲为空时被填充。
_IOLBF行缓冲:对于输出,数据在遇到换行符或者在缓冲填满时被写入,具体视情况而定。对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符。
_IONBF无缓冲:不使用缓冲。每个 I/O 操作都被即时写入。buffer 和 size 参数被忽略。

-size --这是缓冲的大小,以字节为单位。1

将缓冲区禁用之后,我们的串口输入和输出行为符合我们的预期。

顺便提一下,QEMU中所有的运行模式,包括while(1)都会消耗宿主CPU资源,一般会到100%,而STOP模式则不会消耗CPU,因此我们要在必要的时候调用asm("WFI");进入STOP模式。

[1]https://www.runoob.com/cprogramming/c-function-setvbuf.html


  1. 1

相关文章

发表新评论