在 SylixOS 系统中,终端是一种字符型设备,它有多种类型,通常使用 tty 来简称各种类型的终端设备。tty 是 Teletype 的缩写,Teletype 是最早出现的一种终端设备,很像电传打字机,是由 Teletype 公司生产的。
串行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。在 SylixOS 中终端设备是以文件的方式被管理,在系统完成串口设备的创建之后,可以在系统的/dev/目录下看到相应的 tty 设备,一般命名为ttySx(x 表示序号)。对 tty 设备的操作流程与普通文件操作流程相似。
如下图所示:

————————————————————————分割线——————————————————————————————————
文件描述符
文件描述符的本质:内核中,进程打开文件IO信息指针数组的一个下标。 当我们通过描述符操作文件的时候,在pcb中找到files_ struct结构体指针,进而找到结构,找到结构体中的数组,以描述符作为下标获取到文件描述信息的地址,通过描述信息操作文件。

也就是说一个进程运行起来默认会打开三个文件:标准输入,标准输出,标准错误,这三个文件占据的描述符就是: 0-标准输入, 默认从键盘读取信息;1-标准输出,默认将输出结果输出至终端;,2-标准错误,默认将输出结果输出至终端。
/*********************************************************************************************************
** 函数名称: halStdFileInit
** 功能描述: 初始化目标系统标准文件系统
** 输 入 : NONE
** 输 出 : NONE
** 全局变量:
** 调用模块:
*********************************************************************************************************/
#if LW_CFG_DEVICE_EN > 0
static VOID halStdFileInit (VOID)
{
int iFd = open("/dev/ttyS0", O_RDWR, 0);
if (iFd >= 0) {
ioctl(iFd, FIOBAUDRATE, SIO_BAUD_115200);
ioctl(iFd, FIOSETOPTIONS, (OPT_TERMINAL & (~OPT_7_BIT))); /* system terminal 8 bit mode */
ioGlobalStdSet(STD_IN, iFd);
ioGlobalStdSet(STD_OUT, iFd);
ioGlobalStdSet(STD_ERR, iFd);
}
}
#endif /* LW_CFG_DEVICE_EN > 0 */
————————————————————————分割线——————————————————————————————————
回到正题。
串行接口简称串口,也称串行通信接口,是采用串行通信方式的扩展接口。串口是计算机领域最简单的通信接口,也是使用最广泛的通信接口。
串行接口(Serial Interface)是指数据按位顺序传送,其特点是通信线路简单,只要一对传输线就可以实现双向通信,但传送速度较慢,串行通讯的距离可以从几米到几千米。根据信息的传送方向,串行通讯可以进一步分为单工、半双工和全双工三种。单工是指在任何时刻都只能进行单向通信,如进程通信中的管道。半双工是指同一时刻只能进行单方向数据传输,如 I2C 总线,在同一时刻只能进行读或写操作。而全双工是指能够在任何时刻都可以进行双向通信,如串口。
1)波特率:用于衡量通信速度的参数,具体表示每秒钟传送的 bit 的个数。当提到时钟周期时,即指波特率。在通信过程中传输距离和波特率成反比。
2)数据位:用于衡量通信中实际数据位的参数,数据位的标准值有 5/6/7/8 位。数据位的具体设置取决于传送的信息类型。例如,对于标准的 ASCII 码是 0~127,因此数据位可设为 7 位。扩展的 ASCII 码是 0~255,则数据位设置为 8 位。
3)停止位:用于表示单个包的最后一位,典型的值有 1,1.5 和 2 位。停止位不仅表示传输的结束,而且提供计算机校正时钟机会。因此停止位的位数越多,时钟同步的容忍程度越大,同时数据传输率也越慢。
4)校验位:在串口通信中一种简单的检错方式。有四种检错方式:奇/偶校验以及高/低校验。对于奇和偶校验的情况,串口会对校验位进行设置,用该值确保传输的数据有奇数个或者偶数个逻辑高位。对于高/低校验位来说,高位和低位不是用来进行数据的检查,而是简单置位逻辑高或者逻辑低进行校验。
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是设备间进行异步通信的关键模块。其作用如下:
1) 处理数据总路线和串行口之间的串/并、并/串转换;
2) 通信双方只要采用相同的帧格式和波特率,就能在未共享时钟信号的情况下,仅用两根信号线(Rx 和 Tx)就可以完成通信过程;
3)采用异步方式,数据收发完毕后,可通过中断或置位标志位的方式通知微控制器进行处理,大大提高微控制器的工作效率。
相比于异步通信,同步通信需要额外提供时钟线,以保证数据传输时,发送方和接收方能够保持完全的同步,因此,要求接收和发送设备必须使用同一时钟信号。
由于 CPU 与串口缓存区之间数据是按并行方式传输,而串口 FIFO 与外设之间按串行方式传输,因此在串行接口模块中,必须要有“Receive Shifter”(串→并)和“Transmit Shifter” (并→串)。
在数据输入过程中,数据按位从外设进入接口的“Receive Shifter”,当“Receive Shifter” 中已接收完 1 个字符的全部位后,数据就从“Receive Shifter”进入“数据输入寄存器”。CPU从“数据输入寄存器”中读取接收到的字符(并行读取,即 D7~D0 同时被读至累加器中)。 “Receive Shifter”的移位速度由“接收时钟”确定。
在数据输出过程中,CPU 把要输出的字符(并行地)送入“数据输出寄存器”,“数据输出寄存器”的内容传输到“Transmit Shifter”,然后由“Transmit Shifter”移位,把数据按位地送到外设。“Transmit Shifter”的移位速度由“发送时钟”确定,因此,在两个设备之间进行串口通信时,必须将两个设备的波特率设置一致才能正常通信。
接口中的“控制寄存器”用来容纳 CPU 送给此接口的各种控制信息,这些控制信息决定接口的工作方式。
“状态寄存器”的各位称为“状态位”,每一个状态位都可以用来指示数据传输过程中的状态或某种错误。例如,状态寄存器的 D5 位为“1”表示“数据输出寄存器”空,D0 位表示“数据输入寄存器满”,D2 位表示“奇偶检验错”等。
能够完成上述“串/并”转换功能的电路,通常称为“通用异步收发器”( UART:Universal Asynchronous Receiver and Transmitter ),典型的芯片有:Intel 8250/8251,16c550。
在 SylixOS 中,串口驱动的实现有三种方式:轮询、中断、DMA(直接存储器访问)。下面简单介绍这三种方式的特点:
在 SylixOS 中,串口驱动编写需要用户自行管理大部分数据。一般编写顺序是首先初始化串口通道的私有数据,然后进行硬件寄存器的初始化,最后向内核注册串口相关资源,包括串口驱动的实现方法和中断等。
在系统启动过程中,驱动首先会调用 sioChanCreate 创建一个串口通道,该函数是在串口驱动文件中定义的,函数原型如下:
#include
SIO_CHAN *sioChanCreate (INT iChannelNum)
串口通道创建函数主要完成串口的初始化,以及串口中断的安装,并返回串口通道结构体指针。在SylixOS中,使用SIO_CHAN 数据结构来表示一个SIO通道。串口通道结构体定义的详细描述如下:
#include
typedef struct sio_chan {
SIO_DRV_FUNCS *pDrvFuncs;
} SIO_CHAN;
结构体定义的详细描述如下:
#include
struct sio_drv_funcs {
INT (*ioctl)
(SIO_CHAN *pSioChan,
INT cmd,
PVOID arg)
INT (*txStartup)
(SIO_CHAN *pSioChan);
INT (*callbackInstall)
(SIO_CHAN *pSioChan,
INT callbackType,
VX_SIO_CALLBACK callback,
PVOID callbackArg);
INT (*pollInput)
(SIO_CHAN *pSioChan,
PCHAR inChar);
INT (*pollOutput)
(SIO_CHAN *pSioChan,
CHAR outChar);
};
typedef struct sio_drv_funcs SIO_DRV_FUNCS;
对应结构体 sio_drv_funcs,在BASE_T3/libsylixos/SylixOS/system/util/sioLib.h 中定义了对应的宏。
宏 sioIoctl 定义如下:
#define sioIoctl(pSioChan, cmd, arg) \
((pSioChan)->pDrvFuncs->ioctl(pSioChan, cmd, arg))
该宏原型分析:
| 控制命令 | 含义 |
|---|---|
| SIO_BAUD_SET | 设置波特率参数 |
| SIO_BAUD_GET | 获取串口波特率参数 |
| SIO_MODE_SET | 设置通道模式 |
| SIO_MODE_GET | 获取通道模式 |
| SIO_HW_OPTS_SET | 设置线控参数 |
| SIO_HW_OPTS_GET | 获取线控参数 |
| SIO_OPEN | 打开串口命令 |
| SIO_HUP | 关闭串口命令 |
| SIO_SWITCH_PIN_EN_SET | 设置额外模式 |
| SIO_SWITCH_PIN_EN_GET | 获取额外模式 |
通过调用 sioIoctl 并设置相应串口控制命令,可以对串口进行设置。在使用过程中,对串口波特率,线控参数进行设置的情况比较多,关于串口波特率和线控参数设置的简单介绍如下:
1)SIO_BAUD_SET ,波特率设置:驱动通过调用 sioIoctl 并设置 cmd 参数为 SIO_BAUD_SET,然后设置参数 arg 的数值即可修改串口的波特率。常用的波特率的值有 9600,115200 等。
2)SIO_HW_OPTS_SET ,线控参数设置:串口的线控参数较多,常见的线控参数有数据位、停止位、奇偶校验位等。在 /libsylixos/SylixOS/system/util/sioLib.h 文件中定义了下表所示的宏。
| 标志 | 说明 |
|---|---|
| CLOCAL | 忽略调制调解器状态行 |
| CREAD | 启动接收 |
| CS5 | 5 位数据位 |
| CS6 | 6 位数据位 |
| CS7 | 7 位数据位 |
| CS8 | 8 位数据位 |
| HUPCL | 最后关闭时断开 |
| STOPB | 2 位停止位,否则为 1 位 |
| PARODD | 奇校验,否则为偶校验 |
驱动可以通过赋于参数 arg 为 CS8 | STOPB 设置串口为 8 位数据位 2 个停止位。
宏 sioTxStartup 定义如下:
#define sioTxStartup(pSioChan) \
((pSioChan)->pDrvFuncs->txStartup(pSioChan))
该宏原型分析:
因为串口发送数据时需要启动串口,而 sioTxStartup 就是用来启动串口数据发送,当发送成功后会触发串口中断,中断根据缓存中是否还有数据来决定是否继续发送,当缓存清空后串口将恢复初始状态,并等待下一次传送的启动。
宏 sioCallbackInstall 定义如下:
#define sioCallbackInstall(pSioChan, callbackType, callback, callbackArg) \
((pSioChan)->pDrvFuncs->callbackInstall(pSioChan, callbackType, \
callback, callbackArg))
| 回调类型 | 说明 |
|---|---|
| SIO_CALLBACK_GET_TX_CHAR | 获取传输字符 |
| SIO_CALLBACK_PUT_RCV_CHAR | 将接收数据放入终端缓冲区 |
| SIO_CALLBACK_ERROR | 回调出错 |
宏 sioPollInput 定义如下:
#define sioPollInput(pSioChan, inChar) \
((pSioChan)->pDrvFuncs->pollInput(pSioChan, inChar))
该宏原型分析:
宏 sioPollInput 定义如下:
#define sioPollOutput(pSioChan, thisChar) \
((pSioChan)->pDrvFuncs->pollOutput(pSioChan, thisChar))
该宏原型分析:
在 tty 设备驱动创建过程中将会调用上面提供的宏,完成串口的设置以及启动串口数据发送。
在我们之前drivers/uart目录下新建sio.c和sio.h如下图所示:

开始编写sio.c文件内容
/* 首先先定义一个SIO通道全局变量 */
static SIO_CHAN uartSioChan;
/* 再定义一个SIO通道全局变量 */
static SIO_DRV_FUNCS uartSioDrvFunc;
/* 用SIO驱动操作集来初始化SIO通道 */
SIO_CHAN *uartSioChanCreate (VOID)
{
uartSioChan.pDrvFuncs = &uartSioDrvFunc;
return &uartSioChan;
}
/* 实现SIO驱动中的三个操作集接口 */
static SIO_DRV_FUNCS uartSioDrvFunc = {
.ioctl = uartSioIoctl,
.txStartup = uartStartup,
.callbackInstall = uartSioCbInstall,
};
接下来就实现这三个函数。
uartSioIoctl 这个函数需要实现SIO通道的打开、关闭、硬件参数设置、波特率设置等等。此函数成功返回 ERROR_NONE,失败返回 PX_ERROR。
/* 1.uartSioIoctl 这个函数需要实现SIO通道的打开、关闭、硬件参数设置、波特率设置等等*/
static INT uartSioIoctl (SIO_CHAN *pSioChan, INT iCmd, PVOID pArg)
{
switch (iCmd) {
case SIO_BAUD_SET: /* 设置波特率参数 */
*((LONG *)pArg) = 115200;
break;
case SIO_BAUD_GET: /* 获取串口波特率参数 */
*((LONG *)pArg) = 115200;
break;
case SIO_HW_OPTS_SET: /* 设置线控参数 */
*((LONG *)pArg) = CS8; /* 设置8位数据位 */
break;
case SIO_HW_OPTS_GET: /* 获取线控参数 */
*((LONG *)pArg) = CS8;
break;
case SIO_OPEN: /*打开串口命令*/
break;
case SIO_HUP: /* 关闭串口命令 */
break;
default:
_ErrorHandle(ENOSYS);
return (ENOSYS);
}
return (ERROR_NONE);
}
我们用到的控制命令以及其含义
| 控制命令 | 含义 |
|---|---|
| SIO_BAUD_SET | 设置波特率参数 |
| SIO_BAUD_GET | 获取串口波特率参数,我们的波特率是115200 |
| SIO_HW_OPTS_SET | 设置线控参数,设置硬件参数,比如数据位,奇偶校验位,停止位等 |
| SIO_HW_OPTS_GET | 获取线控参数 ,获取当前硬件设置的参数 |
| SIO_OPEN | 打开SIO通道,一般这里会调用硬件的 初始化代码 |
| SIO_HUP | 关闭串口命令 |
uartStartup是SIO驱动中的发送函数, 这个接口的逻辑:就是从系统发送缓冲区中不停的取出数据, 然后调用串口发送接口发送,直到系统发送缓冲区中没有要发送的数据了就返回。此函数成功返回 ERROR_NONE,失败返回 PX_ERROR。
/* 2. uartStartup:SIO驱动中的发送函数了。
* 这个接口的逻辑:就是从系统发送缓冲区中不停的取出数据,
* 然后调用串口发送接口发送,直到系统发送缓冲区中没有要发送的数据了就返回
*/
static INT uartStartup (SIO_CHAN *pSioChan)
{
CHAR cChar;
while (!uartGetTxChar(pTxArg, &cChar)) {
uartPutChar(cChar);
}
return (ERROR_NONE);
}
SylixOS没有为BSP提供相应的API来直接操作系统缓冲区的接口,而是在tty设备创建时将内核中的读写系统缓冲区的函数指针传入SIO驱动中的callbackInstall 接口, 驱动需要自己保存这两个函数指针和其对应的参数,我们可以在驱动中定义全局变量用来保存。
/* 3. uartSioCbInstall SylixOS没有为BSP直接提供操作系统缓冲区的接口,
* 而是在tty设备创建时将内核中的读写系统缓冲区的函数指针传入SIO驱动中的callbackInstall 接口,
* 驱动需要自己保存这两个函数指针和其对应的参数,我们可以在驱动中定义全局变量用来保存 */
static INT (*uartGetTxChar)(PVOID pArg, PCHAR pcChar);
static INT (*uartPutRcvChar)(PVOID pArg, CHAR cChar);
static PVOID pTxArg;
static PVOID pRxArg;
/*
* 在callback回调函数被调用的时候,通过以下方法保存内核中的读写函数指针:
*/
static INT uartSioCbInstall (SIO_CHAN *pSioChan,
INT iCallbackType,
VX_SIO_CALLBACK callbackRoute,
PVOID pvCallbackArg)
{
switch (iCallbackType) {
case SIO_CALLBACK_GET_TX_CHAR: /* 获取传输字符 */
uartGetTxChar = (INT (*)(PVOID, PCHAR))callbackRoute;
pTxArg = pvCallbackArg;
break;
case SIO_CALLBACK_PUT_RCV_CHAR: /* 将接收数据放入终端缓冲区 */
uartPutRcvChar = (INT (*)(PVOID, CHAR))callbackRoute;
pRxArg = pvCallbackArg;
break;
default: /* 回调出错 */
_ErrorHandle(ENOSYS);
return (PX_ERROR);
}
return (ERROR_NONE);
}
#define __SYLIXOS_KERNEL
#include
#include
#include "uart.h"
static SIO_CHAN uartSioChan;
static INT (*uartGetTxChar)(PVOID pArg, PCHAR pcChar);
static INT (*uartPutRcvChar)(PVOID pArg, CHAR cChar);
static PVOID pTxArg;
static PVOID pRxArg;
/* 1.uartSioIoctl 这个函数需要实现SIO通道的打开、关闭、硬件参数设置、波特率设置等等*/
static INT uartSioIoctl (SIO_CHAN *pSioChan, INT iCmd, PVOID pArg)
{
switch (iCmd) {
case SIO_BAUD_SET: /* 设置波特率参数 */
*((LONG *)pArg) = 115200;
break;
case SIO_BAUD_GET: /* 获取串口波特率参数 */
*((LONG *)pArg) = 115200;
break;
case SIO_HW_OPTS_SET: /* 设置线控参数 */
*((LONG *)pArg) = CS8; /* 设置8位数据位 */
break;
case SIO_HW_OPTS_GET: /* 获取线控参数 */
*((LONG *)pArg) = CS8;
break;
case SIO_OPEN: /*打开串口命令*/
break;
case SIO_HUP: /* 关闭串口命令 */
break;
default:
_ErrorHandle(ENOSYS);
return (ENOSYS);
}
return (ERROR_NONE);
}
/* 2. uartStartup:SIO驱动中的发送函数了。
* 这个接口的逻辑:就是从系统发送缓冲区中不停的取出数据,
* 然后调用串口发送接口发送,直到系统发送缓冲区中没有要发送的数据了就返回
*/
static INT uartStartup (SIO_CHAN *pSioChan)
{
CHAR cChar;
while (!uartGetTxChar(pTxArg, &cChar)) {
uartPutChar(cChar);
}
return (ERROR_NONE);
}
/* 3. uartSioCbInstall SylixOS没有为BSP直接提供操作系统缓冲区的接口,
* 而是在tty设备创建时将内核中的读写系统缓冲区的函数指针传入SIO驱动中的callbackInstall 接口,
* 驱动需要自己保存这两个函数指针和其对应的参数,我们可以在驱动中定义全局变量用来保存 */
static INT (*uartGetTxChar)(PVOID pArg, PCHAR pcChar);
static INT (*uartPutRcvChar)(PVOID pArg, CHAR cChar);
static PVOID pTxArg;
static PVOID pRxArg;
/*
* 在callback回调函数被调用的时候,通过以下方法保存内核中的读写函数指针:
*/
static INT uartSioCbInstall (SIO_CHAN *pSioChan,
INT iCallbackType,
VX_SIO_CALLBACK callbackRoute,
PVOID pvCallbackArg)
{
switch (iCallbackType) {
case SIO_CALLBACK_GET_TX_CHAR: /* 获取传输字符 */
uartGetTxChar = (INT (*)(PVOID, PCHAR))callbackRoute;
pTxArg = pvCallbackArg;
break;
case SIO_CALLBACK_PUT_RCV_CHAR: /* 将接收数据放入终端缓冲区 */
uartPutRcvChar = (INT (*)(PVOID, CHAR))callbackRoute;
pRxArg = pvCallbackArg;
break;
default: /* 回调出错 */
_ErrorHandle(ENOSYS);
return (PX_ERROR);
}
return (ERROR_NONE);
}
static SIO_DRV_FUNCS uartSioDrvFunc = {
.ioctl = uartSioIoctl,
.txStartup = uartStartup,
.callbackInstall = uartSioCbInstall,
};
SIO_CHAN *uartSioChanCreate (VOID)
{
uartSioChan.pDrvFuncs = &uartSioDrvFunc;
return &uartSioChan;
}
自此一个最简易的串口驱动我们就完成了,编译一下BSP_T3:
