计算机接口程序(C语言)
本文最后更新于 2024年3月15日 下午
计算机接口程序(C语言)
讲义复习
BUL EE2623 Computer Architecture and Interface
Dr. Hongying Meng
单片机程序概要
程序的主要结构
单片机程序的主要结构:
对于任何的单片机程序,其算法结构由三部分组成:
- 重置(Reset):清空单片机现有的内容并重置单片机设置。
- 初始化(Initialise):声明变量,初始化变量值,指定寄存器的地址或初始化特殊寄存器的值等,用于初始化单片机和接口。
- 主程序(Main Program):单片机执行的主要内容。
单片机的主程序必须写在一个死循环内(while(1) {}
)
数据类型
编译器只支持三种数据类型: Unsigned integers, char, int > 单片机不支持float 和 double 两种数据类型, PIC系列也不支持Signed integers. > int 和 char的区别在于:编译器只支持两个int变量做数学/逻辑运算,而支持多个char变量做数学/逻辑运算。
单片机语法
除了C语言常见的语法之外,单片机还支持如下的特殊表达:
表达 | 说明 | 举例 |
---|---|---|
>> |
比特位右移 | var>>2 |
<< |
比特位左移 | var<<1 |
set_bit(file,bit) |
将制定寄存器(File)的制定比特位(bit)设置为1 | set_bit(porta,0) |
clear_bit(file,bit) |
将制定寄存器(File)的制定比特位(bit)设置为0 | clear_bit(porta,4) |
for(init;cond;mod){} |
循环,内部依次是循环变量的初始值、执行循环的条件、对循环变量的操作 | for(int i=0; i<10 ; i++){} |
asm{} |
汇编语言指令(Boost C) | asm { movlw 0 } |
void interrupt(){} |
中断程序 | void interrupt(){if(portb==0){set_bit{porta,3}}} |
时间控制
对Boost C编译器,执行一次循环(不含循环内的操作)大约需要12条汇编指令,执行一次循环大约需要12us.
开关
开关的种类和连接方式有非常多种:
对于简单电路连接的开关,需要对其进行电路分析以判断输入进单片机(pin)的电压是5v(1)还是0v(0)。
对于旋钮式开关,每一个档位对应了一种比特的情况。比如假设有0-F共16个档位,开关需要连接一个port的4个pin(即需要用4个比特位来反馈),每一个档位都对应了0000到1111的一种情况。
Switch Bounce
开关被按下后弹起的瞬间,其开关的值处于不确定状态,这种情况称为Switch
Bounce,通常是\(10^{-3}s\) 到 \(2 × 10^{-3}s\)左右。
避免Switch Bounce的方法:
- 在pin前加入一个由RC组成的低通滤波器。
- 在pin前使用两个交叉连接的NAND门。
- 在程序中用一个delay跨过Switch Bounce的时间。
键盘
物理原理
当某个键被按下的时候,该按键所在的行列由键盘的按下被导通。
程序思路
- 将行作为port的输入/输出,列作为port的输出/输入(用tris进行控制)
- 初始化port内所有比特位的值为1,当按键被按下,该按键所对应的两个比特位的值被清零。
- 先检查每一行/列是否有按键被按下(if &),如果该行/列有按键被按下,转而检测该行/列的每一列/行。
LED灯/发光二极管
物理原理
发光二极管处于正向偏置时,其两端电压处于不同值,发光二极管会发出不同颜色的光(比如红色是1.6V,白色是3.5V)。
发光二极管通常支持的最大电压在3.5V左右,而单片机的输出电压最大可到5V,为了防止二极管被击穿,应该在二极管前加一个电阻以分压。
此外,PIC开发板上的单个pin的供电可到25mA,但是所有pin上的最大电流不能超过200mA,因此需要用BJT晶体管等方法在二极管的输出端限流。
在PIC开发板上,8个LED灯连接到PortA上,对应了PortA从PA0到PA7的8个比特位。
当比特位为1时,对应的LED灯亮起。
程序思路
只需要使用trisa
将指定LED灯对应的比特位设置为输出后,设定该比特位的值为1即可控制该LED灯亮起。
7(8)位数码管
物理原理
八位数码管的每一个显示笔划(Segment)(7位数码管不包括小数点显示)对应了一个比特位,当该比特位的值为0时,该笔画亮起。
程序思路
创建一个数组其对应位置上的值为该数字应当亮起的显示笔画的比特值,检测输入的值\(x\),然后将数组第\(x\)位上的值传出到port即可。
对于多个数码管组成的阵列:
- 用另一个port来控制使用哪些数码管,当比特位为1时,对应的八位数码管亮起。
- 每一个数码管会在很短的时间亮起熄灭,然后下一个数码管重复,当循环闪亮的频率快到人眼无法识别的时候,表现为所有的数码管都同时亮起。
- 在一次循环之后,微控制器被释放然后计算新的显示值。
LCD/液晶显示面板
结构与原理
针脚(Pin)
液晶显示面板共有11个pin,其中8个用于传输8
bit数据,3个用于控制,这三个pin为:
- RS: Register select,选择寄存器为指令寄存器还是d数据寄存器。
- R/W:Read/Write,
控制数据流的方向是从单片机到LED(通常)还是从LED到单片机。
-
E:Enable,控制数据发送的速度:E对应的值每发生一次变化(10),就会发生1bit的数据传输。
显示面板
显示面板被划分为8x2个区域,每个区域可以显示一个字符。
每个区域所对应了一个地址,第一排显示区域的地址是00到07,第二排显示区域的地址是从40到47。
在显示时需要指定显示的起始位置。
由于DDRAM,因此bit7始终为1,操作码为0xC0+起始区域的地址。
显示与控制
每一个常用字符(字母,数字,常用符号,片假名)和指令都对应了一个8bit值,这8bit被拆分为两段:MSB和LSB分两次发送,但是先发送哪一段取决于LED的型号。(课程中为先发送MSB,再发送LSB)
注意在发送字符/指令时,需要将操作码的头四位清零,发送MSB时,需要将MSB移动到操作码的后四位,再清零其头四位。
发送指令也是8bit,其结构: 0010 | MSB/LSB
发送指令的第一段为固定值0x20,后一段为指令的MSB或LSB。
发送字符时,发送字符的指令结构:
发送MSB: 0011 |MSB
发送LSB: 0010 |LSB
程序思路
LCD在使用前需要对其进行唤醒(初始化显示),唤醒方法为不断的向portb发送两个不同的任意非零比特值。
整个LCD的初始化流程为:
1. 设置adcon1接收来自portA的所有pins的数字信号。
2. 设置portA的PA0,PA1,PA2为输出。
3. 设置portB为输出。
4. 清除显示。
5. 唤醒LED。 6. 设置显示模式:显示n行,8bit数据接口,5x7或0.5x7点阵 7.
开启显示,并设置显示/不显示指针 8. 设置字符的显示模式:从左到右/从右到左
9. 设置指针的起始位置。 10. 清除显示。 固定代码为: 1
2
3
4
5
6
7
8
9adcon1 = 0x06; //设置adcon1接收来自portA的所有pins的数字信号
trisa = 0xf8; //设置portA的PA0,PA1,PA2为输出
trisb = 0x00; //设置PortB为输出
lcd_cmd(1); //清除显示
lcd_init(); //初始化LCD
lcd_cmd(0x38); //设置两行,8比特,5x7点阵
lcd_cmd(0x0c); //设置打开显示,不显示光标
lcd_cmd(0x06); //设置光标右移
lcd_cmd(1); //清除显示
将发送的字符串视为一个数组,每一个字符都要进行:“清零,MSB移位,清除头四位,发送MSB,清除头四位,发送LSB”的操作。
ADC/模数转换器
测量原理
ADC的基本原理为其测量范围内的每一个可测的电压值都对应了一个二进制数。
例如8bit ADC量程为0-5V时,0-5V对应了0000 0000到1111
1111的8bit二进制数。
ADC的精度表示为:\(\frac{测量范围}{2^n}\)。
>
注意电压转换为二进制数时,不能整除的情况下,应当只保留整数部分。
类型
ADC根据实现原理分类为三种类型:
- Flash ADC:每一个可以测量的电压值对应了一个比较器连接,是一种非常低下的ADC。
- 逐次逼近式ADC: 下图中8bit
Register的值的从MSB开始0->1,每一个比特位0->1的变化都会由DAC转换为电压与输入电压进行对比,比较器将两个电压进行对比,判断DAC的电压是否高于输入电压:如果输入电压高于DAC的电压,那么Register中这一比特位的值为1,如果低于DAC的电压,那么Register中这一位比特位的值恢复为0。
调整8bit Register的每一个比特位,直到LSB调整完毕,ADC的转换结束。
- Dual slope ADC:由于不同电压下在固定充电时间内充入的电子数不同,通过测量电容放电的时间即可测量不同的电压。这种ADC测量速度慢,但是相比于上面两者价格相对便宜。
寄存器结构
ADRESH/ADRESL
两个8bit寄存器用于存放10bit的转换结果,又两种存放方式:
这两种方式由ADCON1中的bit7:ADFM进行控制。
ADCON1
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Function | ADFM | x | x | x | PCFG3 | PCFG2 | PCFG1 | PCFG0 |
bit7:ADFM,选择寄存模式:
- 为1时10bit转换结果的bit10和bit9放入ADRESH,剩下比特放入ADRESL。
- 为0时10bit转换结果的bit0和bit1放入ADRESL,剩下比特放入ADRSH。
bit6-bit4:Don't Care
bit3-bit0:
PCFG3-PCFG0,选择ADC的4个pins哪些接收模拟信号(0),哪些接收数字信号(1)。
ADCON0
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Function | ADCS1 | ADCS0 | CHS2 | CHS1 | CHS0 | GO/DONE | x | ADON |
bit7-bit6: ADCS1-ADCS0,选择转换速度
bit5-bit3: CHS2-CHS0,选择当前接受portA的哪一个pin的信号进行转换 bit2:
GO/DONE,转换开始和结束的flag,设定其为1使ADC转换开始,转换结束后会自动复位为0.
bit1: Don't care
bit0: ADON,ADC的总开关,1表示开,0表示关。
代码实现
ADC需要通过给定adcon1和adcon0的值来进行初始化。
转换过程的思想思路为,设置GODONE的值为1,开始转换,等到GODONE的值恢复到0时,获取adresh和adresl中的10比特信息(注意移位的问题)。
计时器
结构
计时器的结构如下图所示:
RA4/T0CK1: PortA的pin4,用于接收输入信号
T0SE:选择下降沿还是上升沿触发计数器计数
Fosc: 单片机内部的时钟信号频率 (Fosc/4表示\(\frac{1}{4}\)时钟信号频率)
T0CS: 复用器,选择用RA4还是时钟信号作为输入
PS2-PS0: Pre-scaler(相当于另一个计数器),用于倍数放大。
PSA: 复用器,选择是否使用Pre-scaler。
TMR0: 计数器。
寄存器结构
TMR0
8bit 计数器,每一个计数信号(1us)会计数一次,8bit计数器每256次计数(也就是每个256us)会发生一次溢出。
INTCON
计数器的状态寄存器。
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Function | O | O | O | O | O | T0IF | O | O |
bit2: TMR0溢出标志,T0IF=1表明TMR0发生了溢出。
OPTION
用于控制计时器:
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Function | O | O | T0CS | T0SE | PSA | PS2 | PS1 | PS0 |
bit7-bit6: 其他功能
bit5: T0CS,选择用RA4还是时钟信号作为输入
bit4: T0SE,选择下降沿还是上升沿触发计数器计数
bit3: PSA, 选择是否开启pre-scaler
bit2-bit0: 选择pre-scaler的倍率
理论
TMR0发生一次overflow的时间: \[时钟周期 × 放大倍率 × (256 × 单次计数所需要的时间 - \text{TMR0}初始值)\]
代码思路
可通过给定tmr0的初始值和重置T0IF为0来实现一个精准的delay。
中断
原理
通常,Programm
Counter(PC)负责指向下一条运行指令的地址,IR执行这个地址对应的指令,PC移动到下一条指令。
如果中断程序存在,当前的PC,W register 和 STATUS
register中的内容会被保存到内存中,然后PC跳转到中断程序所处的位置(固定为PC=4
),执行完中断程序后内存中PC,W
register 和 STATUS register的内容被复原。
每运行一行主循环中的代码,void interrupt{}
中的程序就会被执行一次。
程序框图如下图所示:
应用: PWM 控制马达
原理
马达转速的调节是由输入马达的电压来进行控制的,电压越高,马达转速越快。然而单片机只能输出5v或者0v两种电压,PWM提供了一种解决思路,即在如图所示的一个周期\(T\)内,有一部分时间输入马达的电压为0,另一部分时间输出马达的电压为1,当\(T\)非常小时,输入到马达的电压近似为一个周期内电压的平均值:
\[V_{out}=\frac{1}{T}∫_0^TV_{in}dt=\frac{t}{T}V\]
\(t\)为一个周期内电压为\(V\)时的时间。
代码实现
在中断程序中需要有两个计数器cycle(周期计时器) 和
pulse(马达开启状态的计时器)用于控制时间,他们每运行一次中断都会自动-1,当计数器为0时表明对应的阶段已经结束。
在程序实现上 cycle自减1后,首先需要判断cycle的值是否为0:
如果为0,表明当前的周期已经结束,应当为cycle赋值以初始化下一个周期。
同时为pulse赋值,并使马达处于工作状态以进入下一个周期的开始(马达工作)。
当cycle不为0时,需要判断pulse是否为0:如果pulse的值为0,表明应当进入该周期内马达关闭的阶段,因此关闭马达。如果pulse的值不为0,那么pulse自减1。