探究Linux的outb函数的作用和应用 (linux outb())

Linux是一种使用广泛的开源操作系统,而outb函数则是Linux中的一个重要函数。outb函数是Linux中的输入输出函数,其作用是从CPU将数据输出到指定的端口。

在操作系统中,硬件和软件之间的通讯是通过端口进行的。outb函数就是通过写入指定的端口来实现信息的传输,从而在软件和硬件之间构建一个通讯桥梁。而这种输入输出端口的地址都是由硬件设备携带的,例如硬盘、鼠标、键盘等设备都有自己的输入输出端口。

正常情况下,不推荐直接使用outb函数,因为它是寄存器级别的输入输出操作,容易发生系统崩溃等不可挽回的错误。但是在一些特殊的情况下,使用outb函数则是必不可少的。比如,在使用linux系统时,需要进行一些低级别的驱动程序开发或者底层硬件开发时,就必须要用到outb函数。因为驱动程序开发或者硬件开发需要高效的进行数据传输和操作,这时用到outb函数就可以实现对硬件设备的精确控制。

在Linux系统的底层开发中,outb函数具有非常重要的应用。其中最常见的应用场景是对各类硬盘管理的操作。硬盘是计算机中的重要组成部分,而outb函数则是在Linux下实现硬盘操作的关键。在一些需要直接控制硬盘进行读写操作时,outb函数则成为更佳的选择。

此外,outb函数还具有其他的应用场景,例如在进行嵌入式系统开发时,也可以使用outb函数来实现硬件控制和数据输入输出操作。

总体而言,outb函数是Linux系统下至关重要的输入输出函数。虽然它是一种“危险”的函数,但是同时也是一种非常实用和必要的方法。在进行底层开发和硬件控制时,outb函数可以满足开发人员对底层硬件的精确控制和操作需求,提高技术开发的效率。因此对于需要进行硬件开发的学习者和开发人员而言,理解outb函数的作用和应用是非常必要的。

相关问题拓展阅读:

  • linux 启动时何时初始化console,串口等
  • Linux内核完全注释中CMOS_READ(addr)函数
  • ioremap最多映射多大空间

linux 启动时何时初始化console,串口等

1、LINUX下TTY、CONSOLE、串口之间是怎样的层次关系?具体的函数接口是怎样的?串口是如何被调用的? 

2、printk函数是把信息发送到控制台上吧?如何让PRINTK把信息通过串口送出?或者说系统在什么地方来决定是将信息送到显示器还是串口? 

3、start_kernel中一开始就用到了printk函数(好象是printk(linux_banner什么的),在 这个时候整个内核还没跑起来呢那这时候的printk是如何被调用的?在我们的系统中,系统启动是用的现代公司的BOOTLOADER程序,后来好象跳到了LINUX下的head-armv.s, 然后跳到start_kernel,在bootloader 里串口已经是可用的了,那么在进入内核后是不是要重新设置? 

以上问题可能问的比较乱,因为我自己脑子里也比较乱,主要还是对tty,console,serial之间的关系,特别是串口是如何被调用的没搞清这方面的资料又比较少(就情景分析中讲了罩悉一点),希望高手能指点一二,非常谢!

我最近也在搞这方面的东西,也是写一个串口设备的驱动 

搞了将近一个月了,其中上网找资料,看源代码,什么都做了 

但还是一蹋糊涂的,有些问题还是不明白,希望一起讨论讨论 

在/proc/device(没记错应该是这个文件) 

里面有一个叫serial的驱动,其主设备号是4,次设备号是64-12X(没记错应该是这个范围) 

大家都知道,串口的次设备号是从64开始的,串口1 /dev/ttyS0就对应次设备号64,串口2就对应65 

问题是现在我机上只有两个串口,它注册这么多次设备号来干什么? 

对于一个接在串口1的设备,在我注册驱动的时候 

我是需要自己找一个主设备号呢? 

还是就用主设备号4,次设备号从上面12X的后面选? 

还是就用主设备号4,次设备号64? 

在linux的内核中有一个tty层,我看好像有些串口驱动是从这里开始的 

例如调用tty_register_driver()来注册驱动 

就像在pci子系统里调用pci_register_driver()那样的 

那么,用这种机制来注册的驱动, 

它是直接对串口的端口操作呢(例如用inb(),outb()….之类的) 

还是某些更底层的驱动接口呢? 

这些问题缠了我很久都没解决,搞得最后不得不放弃 

现在转向用户空间的应用程序,看能不能有些更高效的方法来实现 

(在用户空间只能用open(“/dev/ttyS0”, O_RDWR)来实现了)

另外还有,系统里已经为我们实现了串口的驱动 

所以我们在用户空间的程序里直接open(“/dev/ttyS0”)就可用了指铅 

但是现在要写的是接在串口上的设备的驱动 

在内核模块中可不可以包含某个唯闷好头文件,然后就可以直接用串口驱动中的接口呢?

看到你们的问题后,感觉很有典型性,因此花了点工夫看了一下,做了一些心得贴在这里,欢迎讨论并指正: 

1、LINUX下TTY、CONSOLE、串口之间是怎样的层次关系?具体的函数接口是怎样的?串口是如何被调用的? 

tty和console这些概念主要是一些虚设备的概念,而串口更多的是指一个真正的设备驱动Tty实际是一类终端I/O设备的抽象,它实际上更多的是一个管理的概念,它和tty_ldisc(行规程)和tty_driver(真实设备驱动)组合在一起,目的是向上层的VFS提供一个统一的接口通过file_operations结构中的tty_ioctl可以对其进行配置。查tty_driver,你将得到n个结果,实际都是相关芯片的驱动因此,可以得到的结论是(实际情况比这复杂得多):每个描述tty设备的tty_struct在初始化时必然挂如了某个具体芯片的字符设备驱动(不一定是字符设备驱动),可以是很多,包括显卡或串口chip不知道你的ARM Soc是那一款,不过看情况你们应该用的是常见的chip,这些驱动实际上都有而console是一个缓冲的概念,它的目的有一点类似于tty实际上console不仅和tty连在一起,还和framebuffer连在一起,具体的原因看下面的键盘的中断处理过程Tty的一个子集需要使用console(典型的如主设备号4,次设备号1―64),但是要注意的是没有console的tty是存在的

而串口则指的是tty_driver举个典型的例子: 

分析一下键盘的中断处理过程: 

keyboard_interrupt―>handle_kbd_event―>handle_keyboard_event―>handle_scancode 

void handle_scancode(unsigned char scancode, int down) 

…….. 

tty = ttytab? ttytab: NULL; 

if (tty && (!tty->driver_data)) { 

…………… 

tty = NULL; 

…………. 

schedule_console_callback(); 

这段代码中的两个地方很值得注意,也就是除了获得tty外(通过全局量tty记录),还进行了console 回显schedule_console_callbackTty和console的关系在此已经很明了!!! 

2、printk函数是把信息发送到控制台上吧?如何让PRINTK把信息通过串口送出?或者说系统在什么地方来决定是将信息送到显示器还是串口? 

具体看一下printk函数的实现就知道了,printk不一定是将信息往控制台上输出,设置kernel的启动参数可能可以打到将信息送到显示器的效果。函数前有一段英文,很有意思: 

/*This is printk. It can be called from any context. We want it to work. 

* We try to grab the console_sem. If we succeed, it’s easy – we log the output and 

* call the console drivers. If we fail to get the semaphore we place the output 

* into the log buffer and return. The current holder of the console_sem will 

* notice the new output in release_console_sem() and will send it to the 

* consoles before releasing the semaphore. 

* One effect of this deferred printing is that code which calls printk() and 

* then changes console_loglevel may break. This is because console_loglevel 

* is inspected when the actual printing occurs. 

*/ 

这段英文的要点:要想对console进行操作,必须先要获得console_sem信号量如果获得console_sem信号量,则可以“log the output and call the console drivers”,反之,则“place the output into the log buffer and return”,实际上,在代码: 

alinkage int printk(const char *fmt, …) 

va_list args; 

unsigned long flags; 

int printed_len; 

char *p; 

static char printk_buf; 

static int log_level_unknown = 1; 

if (oops_in_progress) { /*如果为1情况下,必然是系统发生crush*/ 

/* If a crash is occurring, make sure we can’t deadlock */ 

spin_lock_init(&logbuf_lock); 

/* And make sure that we print immediately */ 

init_MUTEX(&console_sem); 

/* This stops the holder of console_sem just where we want him */ 

spin_lock_irqsave(&logbuf_lock, flags); 

/* Emit the output into the temporary buffer */ 

va_start(args, fmt); 

printed_len = vsnprintf(printk_buf, sizeof(printk_buf), fmt, args);/*对传入的buffer进行处理,注意还不是 

真正的对终端写,只是对传入的string进行格式解析*/ 

va_end(args); 

/*Copy the output into log_buf. If the caller didn’t provide appropriate log level tags, we insert them here*/ 

/*注释很清楚*/ 

for (p = printk_buf; *p; p++) { 

if (log_level_unknown) { 

if (p != ‘ ‘7’ || p != ‘>’) { 

emit_log_char(”); 

log_level_unknown = 0; 

emit_log_char(*p); 

if (*p == ‘\n’) 

log_level_unknown = 1; 

if (!arch_consoles_callable()) { 

/*On some architectures, the consoles are not usable on secondary CPUs early in the boot process.*/ 

spin_unlock_irqrestore(&logbuf_lock, flags); 

goto out; 

if (!down_trylock(&console_sem)) { 

/*We own the drivers. We can drop the spinlock and let release_console_sem() print the text*/ 

spin_unlock_irqrestore(&logbuf_lock, flags); 

console_may_schedule = 0; 

release_console_sem(); 

} else { 

/*Someone else owns the drivers. We drop the spinlock, which allows the semaphore holder to 

proceed and to call the console drivers with the output which we just produced.*/ 

spin_unlock_irqrestore(&logbuf_lock, flags); 

out: 

return printed_len; 

实际上printk是将format后的string放到了一个buffer中,在适当的时候再加以show,这也回答了在start_kernel中一开始就用到了printk函数的原因 

3、start_kernel中一开始就用到了printk函数(好象是printk(linux_banner什么的),在这个时候整个内核还没跑起来呢。那这时候的printk是如何被调用的?在我们的系统中,系统启动是用的现代公司的BOOTLOADER程序,后来好象跳到了LINUX下的head-armv.s, 然后跳到start_kernel,在bootloader 里串口已经是可用的了,那么在进入内核后是不是要重新设置? 

Bootloader一般会做一些基本的初始化,将kernel拷贝物理空间,然后再跳到kernel去执行。可以肯定的是kernel肯定要对串口进行重新设置,原因是Bootloader有很多种,有些不一定对串口进行设置,内核不能依赖于bootloader而存在。

多谢楼上大侠,分析的很精辟。我正在看printk函数。

我们用的CPU是hynix的hms7202。在评估板上是用串口0作 

控制台,所有启动过程中的信息都是通过该串口送出的。 

在bootloader中定义了函数ser_printf通过串口进行交互。

但我还是没想明白在跳转到linux内核而console和串口尚未 

初始化时printk是如何能够工作的?我看了start_kernel 

的过程(并通过超级终端作了一些跟踪),console的初始化 

是在console_init函数里,而串口的初始化实际上是在1号 

进程里(init->do_basic_setup->do_initcalls->rs_init), 

那么在串口没有初始化以前prink是如何工作的?特别的,在 

start_kernel一开始就有printk(linux_banner),而这时候 

串口和console都尚未初始化呢。

在start_kernel一开始就有printk(linux_banner),而这时候串口和console都尚未初始化? 

仔细分析printk可以对该问题进行解答代码中的: 

/* Emit the output into the temporary buffer */ 

va_start(args, fmt); 

printed_len = vsnprintf(printk_buf, sizeof(printk_buf), fmt, args); 

va_end(args); 

将输入放到了printk_buf中,接下来的 

for (p = printk_buf; *p; p++) { 

if (log_level_unknown) { 

if (p != ‘ ‘7’ || p != ‘>’) { 

emit_log_char(”); 

log_level_unknown = 0; 

emit_log_char(*p); 

if (*p == ‘\n’) 

log_level_unknown = 1; 

则将printk_buf中的内容进行解析并放到全局的log_buf(在emit_log_char函数)中if (!down_trylock(&console_sem)) { 

/* 

* We own the drivers. We can drop the spinlock and let 

* release_console_sem() print the text 

*/ 

spin_unlock_irqrestore(&logbuf_lock, flags); 

console_may_schedule = 0; 

release_console_sem(); 

} else { 

/* 

* Someone else owns the drivers. We drop the spinlock, which 

* allows the semaphore holder to proceed and to call the 

* console drivers with the output which we just produced. 

*/ 

spin_unlock_irqrestore(&logbuf_lock, flags); 

则是根据down_trylock(&console_sem)的结果调用release_console_sem(),在release_console_sem()中才真正的对全局的log_buf中的内容相应的console设备驱动进行处理。至此,可以得到如下的一些结论: 

(1)printk的主操作实际上还是针对一个buffer(log_buf),该buffer中的内容是否显示(或者说向终端输出),则要看是否可以获得console_sem(2)printk所在的文件为printk.c,是和体系结构无关的,因此对任何平台都一样。 可以推测的结论是: 

(1)kernel在初始化时将console_sem标为了locked,因此在start_kernel一开始的printk(linux_banner)中实际只将输入写入了缓冲,等在串口和console初始化后,对printk的调用才一次将缓冲中的内容向串口和console输出。 (2)在串口和console的初始化过程中,必然有对console_sem的up操作。 

(3)因此,在embedded的调试中,如果在console的初始化之前系统出了问题,不会有任何的输出。 唯一可以使用的只能是led或jtag了。(4)因此,你的问题可以看出解答。2.console的初始化. 

不知道你用的是那一个内核版本,在我看的2.4.18和2.4.19中,都是在start_kernel中就对console进行的初始化。从前面的分析来看,console的初始化不应该太晚,否则log_buf有可能溢出。

多谢楼上,分析的很精彩! 

我们用的内核版本是2.4.18,console的初始化确实是在 

start_kernel->console->init。关于tty和串口,我这里还想再问一下tty设备的操作的总入口 

是 

static struct file_operations tty_fops = { 

llseek: no_llseek, 

read: tty_read, 

write: tty_write, 

poll: tty_poll, 

ioctl: tty_ioctl, 

open: tty_open, 

release: tty_release, 

fasync: tty_fasync, 

}; 

而对串口的操作定义在: 

static struct tty_driver serial_driver 这个结构中

serial.c中的多数函数都是填充serial_driver中的函数指针

那么在对串口操作时,应该是先调用tty_fops中的操作(比如 

tty_open等),然后再分流到具体的串口操作(rs_open等)吧? 

但tty_driver(对串口就是serial_driver)中有很多函数指针 

并不跟file_operations中的函数指针对应,不知道这些对应 

不上的操作是如何被执行的?比如put_char,flush_char,read_proc, 

write_proc,start,stop等。

以下是我对这个问题的一些理解: 

Linux内核完全注释中CMOS_READ(addr)函数

直接用应该是可以的,好像后面版本的内核中就没有了0x80。

不可以用ADDR的,

0X80是说那个,,,等一下我查一下书,

ioremap最多映射多大空间

内存映射-IO空间-ioremap-iounremap

( 1 )关于 IO 与内存空间:

在 X86 处理器中存在着 I/O 空间的概念, I/O 空间是相对于内存空间而言的,它通过特定的指令 in 、 out 来访问明誉。端口号标识了外设的寄存器地址。 Intel 语法的 in 、 out 指令格式为:

IN 累加器 ,{ 端口号 │DX}

OUT{ 端口号 │DX}, 累加器

目前,大多数嵌入式微控制器如 ARM 、 PowerPC 等中并不提供 I/O 空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。

即便是在 X86 处理器中,虽然提供了 I/O 空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。此时, CPU 可以像访问一个内存单元那样访问外设 I/O 端口,而不需要设立专门的 I/O 指令。因此,内存空间是必须的,而 I/O 空间是可选的。

( 2 ) inb 和 outb :

在 Linux 设备驱动中,宜使用 Linux 内核提供的函数来访问定位于 I/O 空间的端口,这些函数包括:

· 读写字节端口( 8 位宽)

unsignedinb(unsignedport);

voidoutb(unsignedcharbyte,unsignedport);

· 读写字端口( 16 位宽)

unsignedinw(unsignedport);

voidoutw(unsignedshortword,unsignedport);

· 读写长字端口( 32 位宽)

unsignedinl(unsignedport);

voidoutl(unsignedlongword,unsignedport);

· 读写一串字节

voidin(unsignedport,void*addr,unsignedlongcount);

voidout(unsignedport,void*addr,unsignedlongcount);

·in() 从端口 port 开始读 count 个字节端口,并将读取结果写入 addr 指向的内存; out() 将 addr 指向的内存的 count 个字节连续地写入 port 开始的端口。

· 读写一串字

voidinsw(unsignedport,void*addr,unsignedlongcount);

voidoutsw(unsignedport,void*addr,unsignedlongcount);

· 读写一串长字

voidinsl(unsignedport,void*addr,unsignedlongcount);

voidoutsl(unsignedport,void*addr,unsignedlongcount);

上述各函数中 I/O 端口号 port 的类型高度依赖于具体的硬件平台,因此扮悄,只是写出了 unsigned 。

( 3 ) readb 和 writeb:

在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是工程师宜使用 Linux 内核的如下一组函数来完成设备内存映射的虚拟地址的读写,这些函数包括:

· 读 I/O 内存

unsignedintioread8(void*addr);

unsignedintioread16(void*addr);

unsignedintioread32(void*addr);

与上述函数对应的较早版本的函数为(这些函数在 Linux2.6 中仍然被支持):

unsignedreadb(address);

unsignedreadw(address);

unsignedreadl(address);

· 写 I/O 内存

voidiowrite8(u8value,void*addr);

voidiowrite16(u16value,void*addr);

voidiowrite32(u32value,void*addr);

与上述函数对应的较早版本的函数为(这些函数在 Linux2.6 中仍然被支持):厅槐渣

voidwriteb(unsignedvalue,address);

voidwritew(unsignedvalue,address);

voidwritel(unsignedvalue,address);

( 4 )把 I/O 端口映射到 “ 内存空间 ”:

void*ioport_map(unsignedlongport,unsignedintcount);

通过这个函数,可以把 port 开始的 count 个连续的 I/O 端口重映射为一段 “ 内存空间 ” 。然后就可以在其返回的地址上像访问 I/O 内存一样访问这些 I/O 端口。当不再需要这种映射时,需要调用下面的函数来撤消:

voidioport_unmap(void*addr);

实际上,分析 ioport_map() 的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个 “ 假象” ,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的 I/O 内存访问接口访问 I/O 端口。

11.2.7 I/O 空间的映射

很多硬件设备都有自己的内存,通常称之为 I/O 空间。例如,所有比较新的图形卡都有几 MB 的 RAM ,称为显存,用它来存放要在屏幕上显示的屏幕影像。

1 .地址映射

根据设备和总线类型的不同, PC 体系结构中的 I/O 空间可以在三个不同的物理地址范围之间进行映射:

( 1 )对于连接到 ISA 总线上的大多数设备

I/O 空间通常被映射到从 0xa0000 到 0xfffff 的物理地址范围,这就在 640K 和 1MB 之间留出了一段空间,这就是所谓的 “ 洞 ” 。

( 2 )对于使用 VESA 本地总线( VLB )的一些老设备

这是主要由图形卡使用的一条专用总线: I/O 空间被映射到从 0xe00000 到 0xffffff 的地址范围中,也就是14MB 到 16MB 之间。因为这些设备使页表的初始化更加复杂,因此已经不生产这种设备。

( 3 )对于连接到 PCI 总线的设备

I/O 空间被映射到很大的物理地址区间,位于 RAM 物理地址的顶端。这种设备的处理比较简单。

2 .访问 I/O 空间

内核如何访问一个 I/O 空间单元?让我们从 PC 体系结构开始入手,这个问题很容易就可以解决,之后我们再进一步讨论其他体系结构。

不要忘了内核程序作用于虚拟地址,因此 I/O 空间单元必须表示成大于 PAGE_OFFSET 的地址。在后面的讨论中,我们假设 PAGE_OFFSET 等于 0xc,也就是说,内核虚拟地址是在第 4G 。

内核驱动程序必须把 I/O 空间单元的物理地址转换成内核空间的虚拟地址。在 PC 体系结构中,这可以简单地把 32位的物理地址和 0xc常量进行或运算得到。例如,假设内核需要把物理地址为 0x000b0fe4 的 I/O 单元的值存放在 t1 中,把物理地址为 0xfc的 I/O 单元的值存放在 t2 中,就可以使用下面的表达式来完成这项功能:

t1=*((unsignedchar*)(0xc00b0fe4));

t2=*((unsignedchar*)(0xfc000000));

在第六章我们已经介绍过 , 在初始化阶段 , 内核已经把可用的 RAM 物理地址映射到虚拟地址空间第 4G 的最初部分。因此,分页机制把出现在之一个语句中的虚拟地址 0xc00b0fe4 映射回到原来的 I/O 物理地址 0x000b0fe4 ,这正好落在从 640K 到 1MB 的这段 “ISA 洞 ” 中。这正是我们所期望的。

但是,对于第二个语句来说,这里有一个问题,因为其 I/O 物理地址超过了系统 RAM 的更大物理地址。因此,虚拟地址 0xfc就不需要与物理地址 0xfc相对应。在这种情况下,为了在内核页表中包括对这个 I/O 物理地址进行映射的虚拟地址,必须对页表进行修改:这可以通过调用 ioremap() 函数来实现。 ioremap() 和vmalloc() 函数类似,都调用 get_vm_area() 建立一个新的 vm_struct 描述符,其描述的虚拟地址区间为所请求I/O 空间区的大小。然后, ioremap() 函数适当地更新所有进程的对应页表项。

因此,第二个语句的正确形式应该为:

io_mem=ioremap(0xfb000000,0x202300);

t2=*((unsignedchar*)(io_mem+0x100000));

之一条语句建立一个 2MB 的虚拟地址区间,从 0xfb开始;第二条语句读取地址 0xfc的内存单元。驱动程序以后要取消这种映射,就必须使用 iounmap() 函数。

现在让我们考虑一下除 PC 之外的体系结构。在这种情况下,把 I/O 物理地址加上 0xc常量所得到的相应虚拟地址并不总是正确的。为了提高内核的可移植性, Linux 特意包含了下面这些宏来访问 I/O 空间:

readb,readw,readl

分别从一个 I/O 空间单元读取 1 、 2 或者 4 个字节

writeb,writew,writel

分别向一个 I/O 空间单元写入 1 、 2 或者 4 个字节

memcpy_fromio,memcpy_toio

把一个数据块从一个 I/O 空间单元拷贝到动态内存中,另一个函数正好相反,把一个数据块从动态内存中拷贝到一个I/O 空间单元

memset_io

用一个固定的值填充一个 I/O 空间区域

对于 0xfc000000I/O 单元的访问推荐使用这样的方法:

io_mem=ioremap(0xfb000000,0x202300);

t2=readb(io_mem+0x100000);

使用这些宏,就可以隐藏不同平台访问 I/O 空间所用方法的差异。

linux outb()的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于linux outb(),探究Linux的outb函数的作用和应用,linux 启动时何时初始化console,串口等,Linux内核完全注释中CMOS_READ(addr)函数,ioremap最多映射多大空间的信息别忘了在本站进行查找喔。

版权声明:本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
文章名称:《探究Linux的outb函数的作用和应用 (linux outb())》
文章链接:https://zhuji.vsping.com/28352.html
本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。