作品首发于个人博客
www.thedreamfish.cn
输入/输出设备通常都有模拟或非电组件。显然我们可以意识到cpu通过读写寄存器与设备的通信,这些设备通常有下面这些寄存器。
这里我们使用在51单片机中学习的PCF8591进行举例,通过对其操作字进行说明
发送到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]
在程序中使用设备的最简单的方法是忙等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和其他自动保存的状态,返回被中断的代码继续执行。
中断导致分支的损失。需要额外的时钟周期来应答中断,中断程序需要不断保护和恢复那些不被中断自动保存的寄存器。硬件查找中断和查找中断向量所需要的时间不能由程序员决定,
/*
字符串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。