Console 的基本架构
首先配置需要启动 CONSOLE 驱动,另外还需要文件系统的支持,可以配置 devfs 或者 imfs 都可以。主要是提供一个 /dev/console 的节点注册,访问等支持。
#defineCONFIGURE_APPLICATION_NEEDS_CONSOLE_DRIVER
#defineCONFIGURE_USE_DEVFS_AS_BASE_FILESYSTEM
其中CONFIGURE_APPLICATION_NEEDS_CONSOLE_DRIVER 的配置,那么confdefs.h 中的Device_drivers 数组加入了CONSOLE_DRIVER_TABLE_ENTRY
而这个 entry 的定义是驱动所需要的所有函数,具体如下
#define CONSOLE_DRIVER_TABLE_ENTRY\
{console_initialize, console_open, console_close, \
console_read,console_write, console_control }
作为串口驱动,我们可以完全自己实现以上的几个函数,例子可以看\arm\csb336\console ,(这个例子作为后面用,可以做个了解。),rtems本身也抽象了一个公用的串口驱动,用户只需要实现具体串口相关的函数,libbsp\shared\ 目录下,consoleXXX.c 函数,rtems的 arm bsp 大多使用了第二种方案,例如arm\stm32f4\console
下面以arm\stm32f4\console 为例子说说 poll (死循环查询)的console的实现方法。
从libbsp\shared\ 的console 文件中可以看出,console 的read write 方法其实就是调用termios 系统,所以说,说白了其实读的代码就是 termios。
初始化的流程是:
boot_card --rtems_initialize_device_drivers -- _IO_Initialize_all_drivers -- rtems_io_initialize
在rtems_io_initialize 函数中,根据 _IO_Driver_address_table,顺序的调用每个成员的 initialization_entry 函数。而_IO_Driver_address_table 作为一个系统的 _IO_Manager_initialization中初始化的,其实最后指向的就是 Device_drivers ,也就是我们可以配置的那个,这里的串口就是指CONSOLE_DRIVER_TABLE_ENTRY 所以 initialization_entry其实指向的就是 console_initialize
console_initialize函数做了3件事,首先 rtems_termios_initialize初始化 termios,只是初始化信号量。接着console_initialize_hardware 初始化硬件,最后 rtems_io_register_name注册设备,并且创建 /dev/console 节点,都完成之后 console 设备就已经准备好了。
对于devfs的系统
Write à devFS_writeà rtems_deviceio_write à rtems_io_write
最后根据已经注册的设备的write_entry 找到写句柄,这里是console_write à rtems_termios_write 最后绕到 termios 的写方法上面来了。
写方法,只有一个参数,这个参数是在在rtems_termios_open 中连接到节点的 data1 上面的 args->iop->data1= tty; 所以写方法的时候就可以读出这个节点, tty 结构可以理解为一个对象,包含了这个tty设备的所有信息。
rtems_status_code
rtems_termios_write (void *arg)
{
rtems_libio_rw_args_t *args = arg;
struct rtems_termios_tty *tty = args->iop->data1;
写方法首先得到信号量,确保多线程访问的时候只有一个线程获取信号量。
sc= rtems_semaphore_obtain (tty->osem, RTEMS_WAIT, RTEMS_NO_TIMEOUT);
if(sc != RTEMS_SUCCESSFUL)
return sc;
根据标志位判断,输出流是否需要处理特殊符号,例如 \r \n 等符号,对于console 来说这是需要的,但对于其他串口设备来说,可能他们有自己的处理函数,所以设置标志位的时候需要注意。Oproc 处理完后还是调用rtems_termios_puts 的,所以两个处理的流程差不多。
if (tty->termios.c_oflag & OPOST) {
uint32_t count = args->count;
char *buffer =args->buffer;
while (count--)
oproc (*buffer++, tty);
args->bytes_moved = args->count;
}else {
rtems_termios_puts (args->buffer, args->count, tty);
args->bytes_moved = args->count;
}
在oproc 中处理一些特殊控制符,例如收到 \n 是否需要先发送一个 \r ,\b 回退一个字符等等,处理完之后就输出
在rtems_termios_puts 函数中根据device 设置的是否POLLED 模式,也就是轮训模式,如果是的话,则直接调用串口的写函数 device.write 就是串口提供的自身的写方法,很简单,死循环送出len 个数据就行了。
if (tty->device.outputUsesInterrupts ==TERMIOS_POLLED) {
(*tty->device.write)(tty->minor, buf, len);
return;
}
如果是 POLLED 模式,则以上就足够了,但如果是 interrupt 模式,就比较复杂了。
对 rawOutBuf 进行操作,这是一个环形队列
newHead = tty->rawOutBuf.Head;
while (len) {
放入一个数据后新的头
newHead =(newHead + 1) % tty->rawOutBuf.Size;
关中断,因为 环形缓冲在中断中被使用了,所以先上锁
rtems_termios_interrupt_lock_acquire (tty, level);
头+1 == 尾表示队列已经满了,则重新开中断,等待信号量,这个信号量是在中断里面释放的,中断将数据发出去了就发这个信号量。直到等到信号量为止
while (newHead == tty->rawOutBuf.Tail) {
tty->rawOutBufState = rob_wait;
rtems_termios_interrupt_lock_release (tty, level);
sc = rtems_semaphore_obtain(
tty->rawOutBuf.Semaphore, RTEMS_WAIT, RTEMS_NO_TIMEOUT);
if (sc != RTEMS_SUCCESSFUL)
rtems_fatal_error_occurred (sc);
rtems_termios_interrupt_lock_acquire (tty, level);
}
队列已经有空位了,则放入数据
tty->rawOutBuf.theBuf[tty->rawOutBuf.Head] = *buf++;
tty->rawOutBuf.Head= newHead;
如果之前的缓冲状态是 idle,则判断流控,如果没流控信号,则直接通过 device.write 方法写到串口上面,然后置状态为 busy,注意,这个写方法和之前的POLL模式的写方法是不同的
if (tty->rawOutBufState == rob_idle) {
/* check, whether XOFF has been received */
if (!(tty->flow_ctrl & FL_ORCVXOF)) {
(*tty->device.write)(
tty->minor, &tty->rawOutBuf.theBuf[tty->rawOutBuf.Tail],1);
} else {
/* remember that output has been stopped due to flow ctrl*/
tty->flow_ctrl |= FL_OSTOP;
}
tty->rawOutBufState = rob_busy;
}
重新打开中断
rtems_termios_interrupt_lock_release (tty, level);
len--;
}
循环直到所有数据都发出去为止。
一个中断串口的例子在\libbsp\arm\csb336\console\uart.c
中断模式的写方法,只存放数据的指针,或者写入自己的环形队列,等等方法实现都行,具体看具体的IC,然后开中断,则串口模块就自己发送数据了,发送完之后产生中断
static ssize_t imx_uart_intr_write(int minor,const char *buf, size_t len)
{
if (len > 0) {
imx_uart_data[minor].buf = buf;
imx_uart_data[minor].len = len;
imx_uart_data[minor].idx = 0;
imx_uart_data[minor].regs->cr1 |= MC9328MXL_UART_CR1_TXMPTYEN;
}
return 1;
}
中断的处理函数,
static void imx_uart_tx_isr(void * param)
{
imx_uart_data_t *uart_data = param;
int len;
int minor = uart_data->minor;
判断写缓冲里面还有没有数据,如果有则继续发送
if (uart_data->idx < uart_data->len) {
while ( (uart_data->regs->sr1 & MC9328MXL_UART_SR1_TRDY)&&
(uart_data->idx <uart_data->len)) {
uart_data->regs->txd = uart_data->buf[uart_data->idx];
uart_data->idx++;
}
} else {
没有数据,所有数据都发送完毕了,
len = uart_data->len;
uart_data->len = 0;
imx_uart_data[minor].regs->cr1 &= ~MC9328MXL_UART_CR1_TXMPTYEN;
这里需要重新装填数据
rtems_termios_dequeue_characters(uart_data->tty,len);
}
}
rtems_termios_puts 函数不会等所有数据都发送完毕,而是等所有的数据都放入缓冲,就直接返回的了。然后由设备的中断函数不断的调用rtems_termios_dequeue_characters ,从tty缓冲中取出数据然后发送,直到最后自动完成。
对于写操作rtems_termios_read 则稍微有点不同,
首先准备数据,数据是存放在 cbuf 当中的,根据串口的模式是 poll 还是中断分别调用 fillBufferPoll 和 fillBufferQueue,对于 poll 操作没啥好说的,就是死等。直到遇到回车换行为止。对于中断,应该使用 queue 操作
if (tty->cindex == tty->ccount) {
tty->cindex = tty->ccount = 0;
tty->read_start_column = tty->column;
if (tty->device.pollRead != NULL &&
tty->device.outputUsesInterrupts == TERMIOS_POLLED)
sc = fillBufferPoll (tty);
else
sc = fillBufferQueue (tty);
if (sc != RTEMS_SUCCESSFUL)
tty->cindex = tty->ccount = 0;
}
以上操作完成后直接复制数据到输出buf中,返回读到了多少个数据,所以重点应该是上面的函数
while (count && (tty->cindex< tty->ccount)) {
*buffer++ = tty->cbuf[tty->cindex++];
count--;
}
args->bytes_moved = args->count - count;
对于 fillBufferPoll
首先判断是否标准的tty模式,因为用户可能需要自己来处理串口的消息,并不希望采用
if (tty->termios.c_lflag & ICANON) {
for (;;) {
从串口中读取一个字符,这个应该是非堵塞的,只返回当前的一个字符,没有则返回-1
n = (*tty->device.pollRead)(tty->minor);
if (n < 0) {
没有则等待一个tick
rtems_task_wake_after (1);
} else {
如果读到的是普通字符,则返回0,继续读,因为是标准的,所以必须是读到一行结束或者文件结束符为止的,至于其他控制字符也在 siproc 中处理,例如读到回车,则读取函数就结束了。或者读取到 cbuf慢了,也同样结束。
if (siproc (n, tty))
break;
}
}
}
else {
这个是非标准模式,则需要判断两个东西 VMIN 和 VTIME,这2个可能设置其中一个,也可能同时设置或者同时不设置,VMIN 表示至少读多少个字符,VTIME 表示超时
rtems_interval then, now;
先读出当前的时间
then = rtems_clock_get_ticks_since_boot();
for (;;) {
n = (*tty->device.pollRead)(tty->minor);
读取一个字符,如果错误,表示没有数据
if (n < 0) {
没有数据就判断有没有设定最少读取一个数据,如果有,则应该继续等,一直等到超时为止
if (tty->termios.c_cc[VMIN]) {
if (tty->termios.c_cc[VTIME] && tty->ccount) {
now = rtems_clock_get_ticks_since_boot();
if ((now - then) > tty->vtimeTicks) {
break;
}
}
} else {
如果没有设定最少数据,则判断是否有设置超时,如果都没有设置,则直接错误退出了。如果有超时,则判断当前时间和开始的时间有没有超过预设的时间,有则超时
if (!tty->termios.c_cc[VTIME])
break;
now = rtems_clock_get_ticks_since_boot();
if ((now - then) > tty->vtimeTicks) {
break;
}
}
rtems_task_wake_after (1);
} else {
如果有数据则处理,超过最少数据则立刻返回
siproc (n, tty);
if (tty->ccount >= tty->termios.c_cc[VMIN])
break;
if (tty->termios.c_cc[VMIN] && tty->termios.c_cc[VTIME])
then = rtems_clock_get_ticks_since_boot();
}
}
}
在fillBufferQueue 核心的思想就是:在一个给定的时间内(timeout)从串口接收缓冲rawInBuf 中拿数据,存放在cbuf 中
循环等待
while( wait ) {
/*
* Process characters read from raw queue
*/
输入队列中海油数据 并且 cbuf 不能超过最大容量
while ((tty->rawInBuf.Head != tty->rawInBuf.Tail) &&
(tty->ccount <(CBUFSIZE-1))) {
unsigned char c;
unsigned int newHead;
从输入缓冲中读出一个字符
newHead = (tty->rawInBuf.Head + 1) % tty->rawInBuf.Size;
c = tty->rawInBuf.theBuf[newHead];
tty->rawInBuf.Head = newHead;
如果缓冲的数据取得差不多,超过了 low water level 则重新发 XON 信号,这是流控的处理
if(((tty->rawInBuf.Tail-newHead+tty->rawInBuf.Size)
% tty->rawInBuf.Size)
< tty->lowwater) {
tty->flow_ctrl &= ~FL_IREQXOF;
/* if tx stopped and XON should be sent... */
if (((tty->flow_ctrl & (FL_MDXON | FL_ISNTXOF))
== (FL_MDXON |FL_ISNTXOF))
&& ((tty->rawOutBufState == rob_idle)
|| (tty->flow_ctrl & FL_OSTOP))) {
/* XON should be sent now... */
(*tty->device.write)(
tty->minor, (void *)&(tty->termios.c_cc[VSTART]), 1);
} else if (tty->flow_ctrl & FL_MDRTS) {
tty->flow_ctrl &= ~FL_IRTSOFF;
/* activate RTS line */
if (tty->device.startRemoteTx != NULL) {
tty->device.startRemoteTx(tty->minor);
}
}
}
将接收到的数据送进去siproc 中处理,例如处理一些回车,换行,退格等等的特殊符号
/* continue processing new character */
if (tty->termios.c_lflag & ICANON) {
if (siproc (c, tty))
wait = 0;
} else {
siproc (c, tty);
if (tty->ccount >= tty->termios.c_cc[VMIN])
wait = 0;
}
timeout = tty->rawInBufSemaphoreTimeout;
}
如果
if ( wait ) {
sc = rtems_semaphore_obtain(
tty->rawInBuf.Semaphore, tty->rawInBufSemaphoreOptions, timeout);
if (sc != RTEMS_SUCCESSFUL)
break;
}
}
而具体的驱动中,就没有了 pollread 方法的,应该在中断中调用rtems_termios_enqueue_raw_characters 来确定收到的数据。
static void imx_uart_rx_isr(void * param)
{
imx_uart_data_t *uart_data = param;
char buf[32];
int i=0;
while (uart_data->regs->sr2 & MC9328MXL_UART_SR2_RDR) {
buf[i] = uart_data->regs->rxd & 0xff;
i++;
}
rtems_termios_enqueue_raw_characters(uart_data->tty,buf, i);
}
大概的脉络就分析完毕了,我大概读了2-3天的源代码,结合百度,算是理解清晰了。感觉代码写的真好,将我之前的所有疑问基本都消除了。RTEMS真是个宝藏。
2014-3-13
Etual