自制操作系统 - 串口设备驱动

串口设备驱动

Posted by 王富杰 on Sunday, March 17, 2024

一、串口介绍

串口是一种 IBM-PC 兼容机上的传统通信端口,使用串口连接外设已经被如 USB(Universial Serial Bus) 等新的通信方式所取代。不过串口仍然广泛应用于工业硬件领域,历史上很多拨号上网的调制解调器(猫)通常以串口连接计算机通信。PC 微机的串行通信使用的异步串行通信芯片是 INS 8250 或 NS16450 兼容芯片,统称为 UART(Universial Asynchronous Receiver/Transceiver 通用异步接收发送器)。负责串口的编码和解码工作。

串口驱动对系统开发者来说实现起来要比 USB 简单的多,通常用于调试的目的,无需复杂的硬件操作,可以在操作系统初始化的早期使用串口。我们这里的目的也是把调试的信息放到串口里面。

二、串口编程

对 UART 的编程实际上是对其内部寄存器执行读写操作。因此可将 UART 看作是一组寄存器集合,包含发送、接收和控制三部分。UART 内部有 10 个寄存器,供 CPU 通过 in/out 指令对其进行访问。我们这里不对串口的寄存器进行过多的介绍,可参考 串口设备驱动

2.1、qemu虚拟机配置串口

qemu 虚拟机配置串口 6 的方式如下:

QEMU+= -chardev stdio,mux=on,id=com1 # 字符设备 1
# QEMU+= -chardev vc,mux=on,id=com1 # 字符设备 1
QEMU+= -chardev udp,id=com2,port=6666,ipv4=on # 字符设备 2
QEMU+= -serial chardev:com1 # 串口 1
QEMU+= -serial chardev:com2 # 串口 2

stdio 表示将字符输出到终端 vc 是 qemu 默认的虚拟终端,可以在 TAB (View -> Show Tabs)中打开 udp 用 udp 协议传输字符数据,主要可以用 netcat 来调试。 netcat调式串口使用到的命令如下:

nc -ulp 6666  # 建立服务器端,监听端口号 6666:
nc -u localhost 6666  #  nc 建立客户端,连接刚建好的服务器

三、串口代码实现

串口是也是作为一个设备,被虚拟设备统一管理。我们使用前两个串口,因为前两个串口的端口地址是固定的。

#include <onix/io.h>
#include <onix/interrupt.h>
#include <onix/fifo.h>
#include <onix/task.h>
#include <onix/mutex.h>
#include <onix/assert.h>
#include <onix/device.h>
#include <onix/debug.h>
#include <onix/stdarg.h>
#include <onix/stdio.h>

#define LOGK(fmt, args...) DEBUGK(fmt, ##args)

#define COM1_IOBASE 0x3F8 // 串口 1 基地址
#define COM2_IOBASE 0x2F8 // 串口 2 基地址

#define COM_DATA 0          // 数据寄存器
#define COM_INTR_ENABLE 1   // 中断允许
#define COM_BAUD_LSB 0      // 波特率低字节
#define COM_BAUD_MSB 1      // 波特率高字节
#define COM_INTR_IDENTIFY 2 // 中断识别
#define COM_LINE_CONTROL 3  // 线控制
#define COM_MODEM_CONTROL 4 // 调制解调器控制
#define COM_LINE_STATUS 5   // 线状态
#define COM_MODEM_STATUS 6  // 调制解调器状态

// 线状态
#define LSR_DR 0x1
#define LSR_OE 0x2
#define LSR_PE 0x4
#define LSR_FE 0x8
#define LSR_BI 0x10
#define LSR_THRE 0x20
#define LSR_TEMT 0x40
#define LSR_IE 0x80

#define BUF_LEN 64

typedef struct serial_t
{
    u16 iobase;           // 端口号基地址
    fifo_t rx_fifo;       // 读 fifo
    char rx_buf[BUF_LEN]; // 读 缓冲
    lock_t rlock;         // 读锁
    task_t *rx_waiter;    // 读等待任务
    lock_t wlock;         // 写锁
    task_t *tx_waiter;    // 写等待任务
} serial_t;

static serial_t serials[2];   // 串口表,我们只使用前两个串口,因此指定大小为2

void recv_data(serial_t *serial)
{
    char ch = inb(serial->iobase);
    if (ch == '\r') // 特殊处理,回车键直接换行
    {
        ch = '\n';
    }
    fifo_put(&serial->rx_fifo, ch);
    if (serial->rx_waiter != NULL)
    {
        task_unblock(serial->rx_waiter);
        serial->rx_waiter = NULL;
    }
}

// 中断处理函数
void serial_handler(int vector)
{
    u32 irq = vector - 0x20;
    assert(irq == IRQ_SERIAL_1 || irq == IRQ_SERIAL_2);
    send_eoi(vector);
    serial_t *serial = &serials[irq - IRQ_SERIAL_1];
    u8 state = inb(serial->iobase + COM_LINE_STATUS);

    if (state & LSR_DR) // 数据可读
    {
        recv_data(serial);
    }
    if ((state & LSR_THRE) && serial->tx_waiter)  // 如果可以发送数据,并且写进程阻塞
    {
        task_unblock(serial->tx_waiter);
        serial->tx_waiter = NULL;
    }
}

int serial_read(serial_t *serial, char *buf, u32 count)
{
    lock_acquire(&serial->rlock);
    int nr = 0;
    while (nr < count)
    {
        while (fifo_empty(&serial->rx_fifo))   // 如果队列为空,阻塞当前任务,等待终端唤醒
        {
            assert(serial->rx_waiter == NULL);
            serial->rx_waiter = running_task();
            task_block(serial->rx_waiter, NULL, TASK_BLOCKED);
        }
        buf[nr++] = fifo_get(&serial->rx_fifo);
    }
    lock_release(&serial->rlock);
    return nr;
}

int serial_write(serial_t *serial, char *buf, u32 count)   // 串口写
{
    lock_acquire(&serial->wlock);    // 申请写锁
    int nr = 0;
    while (nr < count)
    {
        u8 state = inb(serial->iobase + COM_LINE_STATUS);
        if (state & LSR_THRE) // 如果串口可写
        {
            outb(serial->iobase, buf[nr++]);
            continue;
        }
        task_t *task = running_task();
        serial->tx_waiter = task;
        task_block(task, NULL, TASK_BLOCKED);
    }
    lock_release(&serial->wlock);
    return nr;
}


void serial_init()   // 初始化串口
{
    for (size_t i = 0; i < 2; i++)   // 我们只使用前两个串口,因此这里写死为2.
    {
        serial_t *serial = &serials[i];
        fifo_init(&serial->rx_fifo, serial->rx_buf, BUF_LEN);   // 初始化队列,用来读写数据
        serial->rx_waiter = NULL;
        lock_init(&serial->rlock);
        serial->tx_waiter = NULL;
        lock_init(&serial->wlock);

        u16 irq;
        if (!i)
        {
            irq = IRQ_SERIAL_1;
            serial->iobase = COM1_IOBASE;
        }
        else
        {
            irq = IRQ_SERIAL_2;
            serial->iobase = COM2_IOBASE;
        }

        outb(serial->iobase + COM_LINE_CONTROL, 0x80);   // 激活 DLAB
        outb(serial->iobase + COM_BAUD_LSB, 0x30);   // 设置波特率因子 0x0030  波特率为 115200 / 0x30 = 115200 / 48 = 2400
        outb(serial->iobase + COM_BAUD_MSB, 0x00);
        outb(serial->iobase + COM_LINE_CONTROL, 0x03);    // 复位 DLAB 位,数据位为 8 位
        outb(serial->iobase + COM_INTR_ENABLE, 0x0d);  // 0x0d = 0b1101   数据可用 + 中断/错误 + 状态变化 都发送中断
        outb(serial->iobase + COM_MODEM_CONTROL, 0b11011);   // 设置回环模式,测试串口芯片
        outb(serial->iobase, 0xAE);                        // 发送字节
        if (inb(serial->iobase) != 0xAE)                    // 收到的内容与发送的不一致,则串口不可用
        {
            continue;
        }
        outb(serial->iobase + COM_MODEM_CONTROL, 0b1011);       // 设置回原来的模式
        set_interrupt_handler(irq, serial_handler);             // 注册中断函数
        set_interrupt_mask(irq, true);                          // 打开中断屏蔽å
        char name[16];
        sprintf(name, "com%d", i + 1);

        device_install(                             // 进行虚拟设备的安装
            DEV_CHAR, DEV_SERIAL, serial, name, 0,
            NULL, serial_read, serial_write);
        LOGK("Serial 0x%x init...\n", serial->iobase);
    }
}

如上所示,为串口驱动的实现,我们可以通过设备读写来操作串口读写,串口的数据存放到队列中进行暂存。

四、串口测试

我们在test系统调用中进行串口的读写测试:

static u32 sys_test()
{
    char ch;
    device_t *device;

    device_t *serial = device_find(DEV_SERIAL, 0);   // 进程串口读
    assert(serial);

    device_t *keyboard = device_find(DEV_KEYBOARD, 0);
    assert(keyboard);
    device_t *console = device_find(DEV_CONSOLE, 0);
    assert(console);

    device_read(serial->dev, &ch, 1, 0, 0);
    // device_read(keyboard->dev, &ch, 1, 0, 0);
    device_write(serial->dev, &ch, 1, 0, 0);
    device_write(console->dev, &ch, 1, 0, 0);

    return 255;
}

如上所示,test系统调用进行键盘读取,并写入到串口和终端。并在makefile中配置串口1为stdio, 串口2为vc。stdio即QEMU 启动的终端。

五、串口输出日志

我们的系统有了 shell 之后,直接将日志输出到控制台,开始有点乱了,可以将日志通过宏去掉,或者将日志输出到串口。另外,bochs 12 好像对串口的支持不太好,目前不知道是为什么所以直接禁了。内核调试的日志是实现函数debugk

void debugk(char *file, int line, const char *fmt, ...)
{
    device_t *device = device_find(DEV_SERIAL, 0);
    if (!device)
    {
        device = device_find(DEV_CONSOLE, 0);
    }

    int i = sprintf(buf, "[%s] [%d] ", file, line);
    device_write(device->dev, buf, i, 0, 0);

    va_list args;
    va_start(args, fmt);
    i = vsprintf(buf, fmt, args);
    va_end(args);

    device_write(device->dev, buf, i, 0, 0);
}

如上所示,通过修改debugk,将日志输出到串口,这样调试日志就打印到了启动qemu的终端。而不再打印到我们操作系统的shell终端。

最后,串口也输出设备,在设备文件初始化也需要在/dev目录下创建串口的设备文件。

「真诚赞赏,手留余香」

WangFuJie Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付