嵌入式设计输入输出I/O设备的典型方法与问题-忙等和中断

作品首发于个人博客

www.thedreamfish.cn

经典的输入输出设备

输入/输出设备通常都有模拟或非电组件。显然我们可以意识到cpu通过读写寄存器与设备的通信,这些设备通常有下面这些寄存器。

  • 数据寄存器:保存设备待处理或已处理的数据。
  • 状态寄存器:提供设备运行的各种状态寄存器。

PCF8591

这里我们使用在51单片机中学习的PCF8591进行举例,通过对其操作字进行说明

操作字

  • PCF8591是具有I2C总线借口的8位AD/DA转换芯片,内部为单一电源供电(2.5~6V),典型值为5V,CMOS工艺。PCF8591有4路AD输入,属逐次比较型,内含采样保持电路;1路8位DA输出,内含DAC数据寄存器。AD/DA转换的最大速率约为11KHz。
  • 在IICa总线中,器件地址必须是起始条件后作为第一个字节发送。发送给PCF8591的第二个字节被存储在控制寄存器,用于控制寄存器的功能。发送给PCF8591的第三个字节被存储到DAC数据寄存器。并使用片上D/A转换成相应的模拟电压。
  • 一个A/D转换周期总是开始于发送一个有效读模式地址给PCF8591之后。A/D转换周期在应答时钟脉冲的后沿被触发。操作分四步:
    • 发送地址字节,选择该器件。
    • 发送控制字节,选择相应通道。
    • 重新发送地址字节,选择该器件。
    • 接收目标通道的数据。

功能描述

  • I2C总线系统中的每一片PCF8591通过发送有效地址到该器件来激活。改地址包括固定部分和可编程部分。可编程部分必须根据地址A0 A2 A2来设置。在I2C总线协议中,地址必须是骑士条件后作为第一个字节发送。地址字节的最后一位用于设置以后数据传输方向的读/写位。

在这里插入图片描述

  • 发送到PCF8591的第二个字节将被存储在控制寄存器。用于控制器件功能。

    • 具体的寄存器配置(指令)就不详细介绍,总结如下:

      adc功能配置,先后发送0x90、0x40+channel(取值0、1、2、3,总共有四个通道)、0x91即可;

      dac功能配置,先后发送0x90、0x40、user_data(8bit)即可;

读写源码

import smbus
import time
bus = smbus.SMBus(1)
#check your PCF8591 address by type in 'sudo i2cdetect -y -1' in terminal.
def setup(Addr):
	global address
	address = Addr

def read(chn): #channel
	if chn == 0:
		bus.write_byte(address,0x40)
	if chn == 1:
		bus.write_byte(address,0x41)
	if chn == 2:
		bus.write_byte(address,0x42)
	if chn == 3:
		bus.write_byte(address,0x43)
	bus.read_byte(address) # dummy read to start conversion
	return bus.read_byte(address)

def write(val):
	temp = val 
	temp = int(temp) # change string to integer
	bus.write_byte_data(address, 0x40, temp)

if __name__ == "__main__":
	setup(0x48)
	while True:
		print 'AIN0 = ', read(0)
		print 'AIN1 = ', read(1)
		tmp = read(0)
		tmp = 5*(tmp/255)
		write(tmp)
#		time.sleep(0.3)

输入/输出原语

微处理器能够通过两种途径为输入输出提供编程支持:I/O指令和内存映射I/O。我们早先学习的X86为输入输出提供了(in和out),这些指令使得I/O设备提供了单独的地址空间。

而我们在ARM系统中,最普遍的方法是通过内存映射-即使提供I/O指令的CPU也能实现内存映射。这样是为每一个I/O设备提供了内存地址。程序使用一般的CPU读写命令来与设备通信。

读操作

#define DEV1 0x100
int peek(char *location){
    
    
	return *location;
}

DEV1  EQU OX100
LDR r1,#DEV1
LDR r0,[r1]

写操作

#define DEV1 0x100
void poke(char *location, char new){
    
    *location)=new;
}

DEV1 EQU 0X100
LDR r1,#DEV1;
LDR r0,#8
STR r0,[r1]

CPU和外设交互的方式

忙等I/O

在程序中使用设备的最简单的方法是忙等I/O。我们知道如果cpu对一台设备执行多重操作,比如像输出设备写若干字符,他必须等待前一个操作结束后,才可以进入下一个操作。所以我们永远不可能在第一个字符串写完前就开始写第二个字符,外设永远不会进行响应。因此通过查询状态寄存器来询问设备是否空闲,这极其重要

输出源码

#define out_char 0X100
#define out_status 0x101
char *mystring = "helloworld";
char *current_char;
current_char=mystring;
while (current_char = '/0'){
    
    
  poke (out_char,*current_char);
  poke(out_status,1);//打开输出设备
  while (peek(out_status)!=0);//外设状态寄存器为1时表示正在写
  current_char++;
}

输入后输出

//当新字符被读取时,输入设备状态为1,读取后,设置为0,就可以开始新的读取
//写字符,将输出状态设置为1,启动,为0时才可以再一次输出
#define in_data 0x100
#define in_status 0x101
#define out_data 0x110
#define out_status ox111
while (1){
    
    
  while (peek(in_status)==0){
    
     //读取状态寄存器
    tmp= (char)peek(in_data);
  }
  poke(out_data,char);
  poke(out_status,1);
  while (peek(out_status)!=0);
}

中断

我们可以发现,使用忙等将会造成cpu陷入一种极其麻烦的状态,cpu不得不花费大量的精力去不断查询寄存器的状态,无法去处理其他可能更重要的事情。为了使得cpu可以在这一过程中控制其他的I/O设备,或者决定下一个发送到设备的输出数据或处理最后一个接受输入时,进行计算操作。

笔者在参加电子设计竞赛中就遇到过,如果不对ADC/DAC进行中断处理,同时不使用使用DMA采样技术,不仅会使得cpu在运行中出现奇怪的delay延时问题,还会导致采样率精度不高。尤其是在使用DAC输出时,只有使用中断和dma处理后,我们才可以发现原来这样可以极大程度的减少。

显然有些时候,中断虽然具备实时性,快速反应的一系列优点,但是我们知道实际上我们不能总是打断cpu的工作,高频次的打断cpu工作,会破坏流水线等一系列工作的稳定性。

中断开销

中断的过程
  • cpu在指令的开始会检查未决的中断。它响应优先级最高的中断。

  • 设备在接受中断应答信号后,会向cpu发送中断向量。

  • cpu用向量作为索引在中断向量表中查找中断服务程序的地址。并保护中断现场,保存cpu寄存器的状态。

  • 软件将会驱动保存其他cpu状态,之后会执行设备需要的操作,最后执行中断

  • cpu会恢复pc和其他自动保存的状态,返回被中断的代码继续执行。

ARM7响应中断
  • 保存适当的pc值。
  • 将cpsr复制到spsr
  • 强制cpsr中的位记录下中断
  • 强制pc指向中断向量
ARM7结束中断
  • 恢复pc的正确值
  • 用spsr恢复cpsr
  • 清除中断禁用标志
中断的开销

中断导致分支的损失。需要额外的时钟周期来应答中断,中断程序需要不断保护和恢复那些不被中断自动保存的寄存器。硬件查找中断和查找中断向量所需要的时间不能由程序员决定,

中断源码

/*
字符串io_buff保存那些已经读入但是未能写出的字符队列。
当新字符被读取时,输入设备状态为1,读取后,设置为0,就可以开始新的读取
写字符,将输出状态设置为1,启动,为0时才可以再一次输出
我们已经具有下面这些函数
*/
#define buf_size 8
void add_char(char x);
char remove_char();
bool enpty();
bool full();

#define in_data 0X100
#defien in_status 0x101
#define out_data 0x110
#define out_status 0x101

void input_hander(){
    
    
  char tmp;
  tmp=peek(in_data);
  add_char(tmp);
  poke (in_status,0);
  
  if (nchar()==1){
    
    
    poke(out_data,remove_char());
    poke(out_status,1);
  }
}
如果io_buff有字符在等待,输出设备可以自行开启一个输出设备
否则必须有输入设备在其中断程序中启动输出设备。


//输出一个字符串后触发中断
void output_handere(){
    
    
   if (empty()){
    
    
     poke(out_data,remove_char());
     poke(out_atatus,1);
   }
}


//另一种写法
void input hander(){
    
    
  achar=peek(in_data);
  where=1
  poke(in_status,0);
}

int main(){
    
    
	while(1){
    
    
  	if (where){
    
    
      poke(out_data,achar);
      poke(out_status,1);
      where=0;
    }
}}
//1.输入输出的速度可能不一样
//前台参与了工作

管态,异常和陷阱

异常

异常是一种内部可以检测的错误。一个经典的例子是0做除数,未定义的Resets指令和非法的内存访问。异常必须有优先级,因为一个操作可能会产生很多异常,异常的向量好通常由体系结构预先定义;被用于索引异常处理程序表。

陷阱

软件中断,通常会进入管态,如果用户和管态之间的接口没有被仔细设计,用户程序可能偷偷进入管态进行破坏。

管态

程序通常运行在用户态,管态拥有更高的特权。例如,允许动态更改内存单元的物理地址。此模式下,cpsr后五位均设置为1。


猜你喜欢

转载自blog.csdn.net/xrk00/article/details/122510584