UART串口

通用异步收发传输器(Universal Asynchronous Receiver/Transmitter,通常称为UART)是一种异步收发传输器,是电脑硬件的一部分,将数据透过串列通信进行传输。

UART 控制器具有以下特性:

  • 全双工异步数据接收/发送
  • 可编程的数据格式
  • 16 位可编程时钟计数器
  • 支持接收超时检测
  • 带仲裁的多中断系统
  • 仅工作在 FIFO 方式
  • 在寄存器与功能上兼容 NS16550A

在UART上追加同步方式的串行信号变换电路的产品,被称为USART。

在实验初期,由于没有涉及到显示设备,因此如果想要知道系统运行过程中发生的事情就需要我们手动将其打印出来,而这个时候能用的就只有uart串口设备,通过此外设,可以将程序的输出打印到屏幕上。

通常各个厂商的uart都会兼容NS16550,而此设备只需要几个寄存器进行控制,根据不同架构,访问这些寄存器有两种方式,一种是通过端口映射的I/O访问,这种情况一般需要特殊的I/O指令,另一种方式是使用MMIO,这种情况下没有特殊指令,使用一般的访存指令即可。

UART硬件受一个内部时钟信号控制。该时钟信号是数据传输率的倍频,典型是比特率的8或16倍。接收器在每个时钟脉冲时测试接收到的信号状态是否为开始比特。如果开始比特的低电平持续传输1个比特所需时间的一半以上,则认为开始了一个数据帧的传输;否则,则忽略此脉冲信号。到了下一个比特时间后,线路状态被采样并送入移位寄存器。约定的表示一个字符的所有数据比特(典型为5至8个比特)接收后,移位寄存器可被接收系统使用。UART将设置一个标记指出新数据可用,并产生一个处理器中断请求主机处理器取走接收到的数据。

loongArch 结构下,IO 地址空间与内存地址空间统一编址。处理器上运行的指令使用虚拟地址,虚拟地址通过地址映射规则与物理地址相关联。基本的虚拟地址属性首先区分为经缓存(Cache)与不经缓存(Uncache)两种。对于内存操作,现代高性能通用处理器都采用 Cache 方式进行访问,以提升访存性能。对于存储器来说,在 Cache 中进行缓存是没有问题的,因为存储器所存储的内容不会自行修 改。但是对于 IO 设备来说,因为其寄存器状态是随着工作状态的变化而变化的,如果缓存在 Cache 中,那么处理器核将无法得到状态的更新,所以一般情况下不能对 IO 地址空间进行 Cache 访问,需要使用 Uncache访问。使用 Uncache 访问对 IO 进行操作还有另一个作用,就是可以严格控制读写的访问顺序,不会因为预取类的操作导致寄存器状态的丢失。

rCore中,串口的访问依靠了SBI进行实现,在 loongArch 下,则仍然需要编写少量代码,由于在UEFI中已经对串口设备进行了初始化设置工作,因此我们没有必要再次对其进行再次设置。通常,uart设备的初始化过程如下所示:

LEAF(initserial)
# 加载串口设备基地址
li a0, GS3_UART_BASE
#线路控制寄存器,写入0x80(128)表示后续的寄存器访问为分频寄存器访问
li t1, 128
sb.b t1, a0, 3
# 配置串口波特率分频,当串口控制器输入频率为33MHz,将串口通讯速率设置在115200
# 时,分频方式为33,000,000 / 16 / 0x12 = 114583。 由于串口通信有固定的起始格式,
# 能够容忍传输两端一定的速率差异,只要将传输两端的速率保持在一定的范围之内就可
# 以保证传输的正确性
li t1, 0x12
sb.b t1, a0, 0
li t1, 0x0
sb.b t1, a0, 1
# 设置传输字符宽度为8,同时设置后续访问常规寄存器
li t1, 3
sb.b t1, a0, 3
# 不使用中断模式
li t1, 0
sb.b t1, a0, 1
li t1, 71
sb t1, a0, 2
jirl ra
nop
END(initserial)

串口设备使用相同的地址映射了两套功能完全不同的寄存器,通过线路控制寄存器的最高位(就是串口寄存器中偏移为 3 的寄存器的最高位)进行切换。因为其中一套寄存器主要用于串口波特率的设置,只需要在初始化时进行访问,在正常工作状态下完全不用再次读写,所以能够将其访问地址与另一套正常工作用的寄存器相复用来节省地址空间。

在初始化时,代码中先将 0x3 偏移寄存器的最高位设置为 1,以访问分频设置寄存器,按照与连接设备协商好的波特率和字符宽度,将初始化信息写入配置寄存器中。然后退出分频寄存器的访问模式,进入正常工作模式。在使用时,串口的对端是一个同样的串口,两个串口的发送端和接收端分别对连,通过双向的字符通信来实现被调试机的字符输出和字符输入功能。

在内核中如果想要使用串口进行输出,需要完成下面的代码:

#![allow(unused)]
fn main() {
pub fn put(&mut self, c: u8) {
        let mut ptr = self.base_address as *mut u8;
        loop {
            unsafe {
                let c = ptr.add(5).read_volatile();
                if c & (1<<5)!=0{
                    break;
                }
            }
        }
        ptr = self.base_address as *mut u8;
        unsafe {
            ptr.add(0).write_volatile(c);
        }
    }

    pub fn get(&mut self) -> Option<u8> {
        let ptr = self.base_address as *mut u8;
        unsafe {
            if ptr.add(5).read_volatile() & 1 == 0 {
                // The DR bit is 0, meaning no data
                None
            } else {
                // The DR bit is 1, meaning data!
                Some(ptr.add(0).read_volatile())
            }
        }
    }
}

在打印字符函数中,需要偏移地址为0x5的线路状态寄存器,检查FIFO空标志,非空时等待,否则就写入。在读取函数中则相反。

在qemu模拟的loongarch平台上,uart的基地址为 0x1fe001e0.