「正点原子STM32Mini板资料连载」第三十章 PS2 鼠标实验
1)实验平台:正点原子STM32mini开发板2)摘自《正点原子 STM32 不完全手册(HAL 库版)》 关注官方微信号公众号,获取更多资料:正点原子
第三十章 PS2 鼠标实验
PS/2 作为电脑的标准输入接口,用于鼠标键盘等设备。PS/2 只需要一个简单的接口(2 个
IO 口),就可以外扩鼠标、键盘等,是单片机理想的输入外扩方式。
ALIENTEK MiniSTM32 开发板也自带了一个 PS/2 接口,可以用来驱动标准的鼠标、键盘
等外设,也可以用来驱动一些 PS/2 接口的小键盘,条码扫描枪等。在本章中,我们将向大家介
绍,如何在 ALIENTEK MiniSTM32 开发板上,通过 PS/2 接口来驱动电脑鼠标。本章分为如下
几个部分:
30.1 PS/2 简介
30.2 硬件设计
30.3 软件设计
30.4 下载验证
30.1 PS/2 简介
PS/2 是电脑上常见的接口之一,用于鼠标、键盘等设备。一般情况下,PS/2 接口的鼠标为
绿色,键盘为紫色。
PS/2 接口是输入装置接口,而不是传输接口。所以 PS2 口根本没有传输速率的概念,只有
扫描速率。在 Windows 环境下,ps/2 鼠标的采样率默认为 60 次/秒,USB 鼠标的采样率为 120
次/秒。较高的采样率理论上可以提高鼠标的移动精度。
物理上的 PS/2 端口可有 2 种,一种是 5 脚的,一种是六脚的。下面给出这两种 PS/2 接口
的引脚定义图,如图 30.1.1 所示:
图 30.1.1 PS/2 引脚定义图
从图 30.1.1 可以看出,不管是 5 脚还是 6 脚的 PS/2 接头,都是有 4 根有用的线连接:时钟
线、数据线、电源线、地线。PS/2 设备的电源是 5V 的,而数据线和时钟线均是集电极开路的,
这两根信号线都需要接一个上拉电阻(开发板上使用的是 10K)。
PS/2 鼠标和键盘遵循一种双向同步串行协议,换句话说每次数据线上发送一位数据并且每
在时钟线上发一个脉冲就被读入。键盘/鼠标可以发送数据到主机,而主机也可以发送数据到设
备,但主机总是在总线上有优先权,它可以在任何时候抑制来自于键盘/鼠标的通讯,只要把时
钟拉低即可。
从设备到主机的数据在时钟信号的下降沿被主机读取,而从主机到设备的数据在时钟信号
的上升沿被设备读取。不论通信方向如何,时钟总是由设备产生的,最大的时钟频率为 33Khz,
大多数设备工作在 10~20Khz。
鼠标键盘,采用的是一种每帧包含 11/12 位的串行协议,这些位的含义如表 30.1.1 所示:
表 30.1.2 鼠标/键盘帧数据格式
表 30.1.2 中校验位的含义是:如果数据位中包含偶数个 1,则校验位为 1;如果数据位中
包含奇数个 1,则校验位为 0。数据位中的 1 的个数加上校验位总为奇数(奇校验),用于数据
侦错。当主机发送数据给键盘/鼠标的时候,设备会发送一个握手信号来应答数据已经被收到了,
该位不会出现在设备到主机的通信中。
设备到主机的通信过程:
正常情况下数据线和时钟线都是高电平,当键盘/鼠标有数据要发送时,它先检测时钟线,
确认时钟线是高电平。如果不是,则是主机抑制了通信,设备必须缓冲任何要发送的数据,直
到重新获得总线的控制权(键盘有 16 字节的缓冲区而鼠标的缓冲区仅存储最后一个要发送的
数据包)。如果时钟线是高电平,设备就可以开始传送数据了。
设备到主机的数据在时钟线的下降沿被主机读入,如图 30.1.2 所示:
图 30.1.2 设备到主机通信时序图
主机可以在设备发送数据的时候拉低时钟线来来放弃当前数据的传送。
主机到设备的通信过程:
主机到设备的通信与设备到主机的通信有点不同,因为 PS/2 的时钟总是由设备产生的,如
果主机要发送数据,则它必须首先把时钟线和数据线设置为请求发送状态。请求发送状态通过
如下过程实现:
1.拉低时钟线至少 100us 以抑制通信。
2.拉低数据线,以应用“请求发送”,然后释放时钟线。
设备在不超过 10ms 的时间内就会检测这个状态,当设备检测到这个状态后,它将开始产生
时钟信号,并且在设备提供的时钟脉冲驱动下输入八个数据位和一个停止位。主机仅当时钟线
为低的时候改变数据线,而数据在时钟脉冲的上升沿被锁存,这与发生在设备到主机通讯的过程中正好相反。主机到设备的通信时序图如图 30.1.3 所示:
图 30.1.3 主机到设备通信时序图
以上简单介绍了 PS/2 协议的通信过程,更多的介绍请参考《PS/2 技术参考》一文。本章
我们要驱动一个 PS/2 鼠标,所以接下来简单介绍一下 PS/2 鼠标的相关信息。
标准的 PS/2 鼠标支持下面的输入:X(左右)位移、Y(上下)位移、左键、中键和右键。
但是我们目前用到鼠标大都还有滚轮,有的还有更多的按键,这就是所谓的 Intellimouse。它支
持 5 个鼠标按键和三个位移轴(左右、上下和滚轮)。
标准的鼠标有两个计数器保持位移的跟踪:X 位移计数器和 Y 位移计数器。可存放 9 位的 2 进制补码,并且每个计数器都有相关的溢出标志。它们的内容连同三个鼠标按钮的状态一起以三字节移动数据包的形式发送给主机,位移计数器表示从最后一次位移数据包被送往主机后所发生的位移量。
标准 PS/2 鼠标发送唯一和按键信息以 3 字节的数据包格式发给主机,三个数据包的意义如
图 30.1.4 所示:
图 30.1.4 标准鼠标位移数据包格式
位移计数器是一个 9 位 2 的补码整数,其最高位作为符号位出现在位移数据包的第一个字
节里。这些计数器在鼠标读取输入发现有位移时被更新。这些值是自从最后一次发送位移数据
包给主机后位移的累计量(即最后一次包发给主机后位移计数器被复位位移计数器可表示的值
的范围是-255 到+255)。如果超过了范围,相应的溢出位就会被置位,并在复位之前,计数器
不会再增减。
而所谓的 Intellimouse,因为多了 2 个按键和一个滚轮,所以 Intellimouse 的一个位移数据
包由 4 个字节组成,如图 30.1.5 所示:
图 30.1.5 Intellimouse 鼠标位移数据包格式
Z0-Z3 是 2 的补码,用于表示从上次数据报告以来滚轮的位移量。有效范围从-8 到+7,第
四键如果按下,则 4th Btn 位被置位,如果没有按下,则 4th Btn 位为 0。第五键也与此类似。
鼠标的介绍我们就简单的介绍到这里,详细的说明请参考光盘《PS/2 技术参考》第三章
PS/2 鼠标接口(第 36 页)。
30.2 硬件设计
本章实验功能简介:开机的时候先检测是否有鼠标接入,如果没有/检测错误,则提示错误
代码。只有在检测到 PS/2 鼠标之后才开始后续操作,当检测到鼠标之后,就在 LCD 上显示鼠
标位移数据包的内容,并转换为坐标值,在 LCD 上显示,如果有按键按下,则会提示按下的是
哪个按键。同样我们也是用 LED0 来指示程序正在运行。
所要用到的硬件资源如下:
1) 指示灯 DS0
2) TFTLCD 模块
3) PS/2 鼠标
本章需要用到一个PS/2接口的鼠标,大家得自备一个。下面我来看一看开发板上的PS/2
接口与STM32的连接电路,如图30.2.1所示:
图 30.2.1 PS/2 接口与 STM32 的连接电路图
可以看到,PS/2 接口与 STM32 的连接仅仅 2 个 IO 口,其中 PS_CLK 连接在 PA15 上面,
而 PS_DAT 则连接在 PC5 上面,这两个口和 KEY1 和 KEY0 复用了,所以在按键使用的时候,
就不能使用 PS/2 设备了,这个在使用的时候大家要注意一下。
30.3 软件设计
打开上一章的工程,由于本章没有用到 SPI 接口、按键以及 NRF24L01 模块,所以,先去
掉 spi.c、key.c 和 24l01.c(此时 HARDWARE 组仅剩下:led.c 和 ILI93xx.c)。
然后,在 HARDWARE 文件夹下新建 PS2 和 MOUSE 两个文件夹。在 PS2 文件夹里面新建
ps2.c 和 ps2.h 两个文件。然后在 MOUSE 文件夹下新建 mouse.c 和 mouse.h 两个文件。并将这
个两个文件夹加入头文件包含路径。
打开 ps2.c,输入如下代码:
#include "ps2.h"
#include "usart.h"
//PS2_Status 当前状态标志
//[7]:接收到一次数据;[6]:校验错误;[5:4]:当前工作的模式;[3:0]:收到的数据长度;
u8 PS2_Status=CMDMODE; //默认为命令模式
u8 PS2_DATA_BUF[16]; //ps2 数据缓存区
//位计数器
u8 BIT_Count=0;
//中断 15~10 处理函数
//每 11 个 bit,为接收 1 个字节
//每接收完一个包(11 位)后,设备至少会等待 50ms 再发送下一个包
//只做了鼠标部分,键盘部分暂时未加入
//CHECK OK 2017/6/13
void EXTI15_10_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_15);
//调用中断处理公用函数
}
//中断服务程序中需要做的事情
//在 HAL 库中所有的外部中断服务函数都会调用此函数
//GPIO_Pin:中断引脚号
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
static u8 tempdata=0;
static u8 parity=0;
if(GPIO_Pin==GPIO_PIN_15)
{
if(BIT_Count==0)
{
parity=0;
tempdata=0;
}
BIT_Count++;
if(BIT_Count>1&&BIT_Count<10)//这里获得数据
{
tempdata>>=1;
if(PS2_SDA)
{
tempdata|=0x80;
parity++;//记录 1 的个数
}
}else if(BIT_Count==10)//得到校验位
{
if(PS2_SDA)parity|=0x80;//校验位为 1
}
if(BIT_Count==11)//接收到 1 个字节的数据了
{
BIT_Count=parity&0x7f;//取得 1 的个数
if(((BIT_Count%2==0)&&(parity&0x80))||((BIT_Count%2==1)
&&(parity&0x80)==0))//奇偶校验 OK
{
//PS2_Status|=1<<7;//标记得到数据
BIT_Count=PS2_Status&0x0f;
PS2_DATA_BUF[BIT_Count]=tempdata;//保存数据
if(BIT_Count<15)PS2_Status++; //数据长度加 1
BIT_Count=PS2_Status&0x30; //得到模式
switch(BIT_Count)
{
case CMDMODE://命令模式下,每收到一个字节都会产生接收完成
PS2_Dis_Data_Report();//禁止数据传输
PS2_Status|=1<<7; //标记得到数据
break;
case KEYBOARD:
break;
case MOUSE:
if(MOUSE_ID==0)//标准鼠标,3 个字节
{
if((PS2_Status&0x0f)==3)
{
PS2_Status|=1<<7;//标记得到数据
PS2_Dis_Data_Report();//禁止数据传输
}
}else if(MOUSE_ID==3)//扩展鼠标,4 个字节
{
if((PS2_Status&0x0f)==4)
{
PS2_Status|=1<<7;//标记得到数据
PS2_Dis_Data_Report();//禁止数据传输
}
}
break;
}
}else
{
PS2_Status|=1<<6;//标记校验错误
PS2_Status&=0xf0;//清除接收数据计数器
}
BIT_Count=0;
}
}
}
//禁止数据传输
//把时钟线拉低,禁止数据传输
void PS2_Dis_Data_Report(void)
{
PS2_Set_Int(0); //关闭中断
PS2_SET_SCL_OUT();//设置 SCL 为输出
PS2_SCL_OUT=0; //抑制传输
}
//使能数据传输
//释放时钟线
void PS2_En_Data_Report(void)
{
PS2_SET_SCL_IN(); //设置 SCL 为输入
PS2_SET_SDA_IN(); //SDA IN
PS2_SCL_OUT=1; //上拉
PS2_SDA_OUT=1;
PS2_Set_Int(1); //开启中断
}
//PS2 中断屏蔽设置
//en:1,开启;0,关闭;
void PS2_Set_Int(u8 en)
{
EXTI->PR=1<<15; //清除 LINE15 上的中断标志位
if(en)EXTI->IMR|=1<<15;//不屏蔽 line15 上的中断
else EXTI->IMR&=~(1<<15);//屏蔽 line15 上的中断
}
//等待 PS2 时钟线 sta 状态改变
//sta:1,等待变为 1;0,等待变为 0;
//返回值:0,时钟线变成了 sta;1,超时溢出;
u8 Wait_PS2_Scl(u8 sta)
{
u16 t=0;
sta=!sta;
while(PS2_SCL==sta)
{
delay_us(1);
t++;
if(t>16000)return 1;//时间溢出 (设备会在 10ms 内检测这个状态)
}
return 0;//被拉低了
}
//在发送命令/数据之后,等待设备应带,该函数用来获取应答
//返回得到的值
//返回 0,且 PS2_Status.6=1,则产生了错误
u8 PS2_Get_Byte(void)
{
u16 t=0;
u8 temp=0;
while(1)//最大等待 55ms
{
t++;
delay_us(10);
if(PS2_Status&0x80)//得到了一次数据
{
temp=PS2_DATA_BUF[PS2_Status&0x0f-1];
PS2_Status&=0x70;//清除计数器,接收到数据标记
break;
}else if(t>5500||PS2_Status&0x40)break;//超时溢出/接收错误
}
PS2_En_Data_Report();//使能数据传输
return temp;
}
//发送一个命令到 PS2.
//返回值:0,无错误,其他,错误代码
u8 PS2_Send_Cmd(u8 cmd)
{
u8 i;
u8 high=0;//记录 1 的个数
PS2_Set_Int(0); //屏蔽中断
PS2_SET_SCL_OUT();//设置 SCL 为输出
PS2_SET_SDA_OUT();//SDA OUT
PS2_SCL_OUT=0;//拉低时钟线
delay_us(120);//保持至少 100us
PS2_SDA_OUT=0;//拉低数据线
delay_us(10);
PS2_SET_SCL_IN();//释放时钟线,这里 PS2 设备得到第一个位,开始位
PS2_SCL_OUT=1;
if(Wait_PS2_Scl(0)==0)//等待时钟拉低
{
for(i=0;i<8;i++)
{
if(cmd&0x01)
{
PS2_SDA_OUT=1;
high++;
}else PS2_SDA_OUT=0;
cmd>>=1;
//这些地方没有检测错误,因为这些地方不会产生死循环
Wait_PS2_Scl(1);//等待时钟拉高 发送 8 个位
Wait_PS2_Scl(0);//等待时钟拉低
}
if((high%2)==0)PS2_SDA_OUT=1;//发送校验位 10
else PS2_SDA_OUT=0;
Wait_PS2_Scl(1); //等待时钟拉高 10 位
Wait_PS2_Scl(0); //等待时钟拉低
PS2_SDA_OUT=1; //发送停止位 11
Wait_PS2_Scl(1);//等待时钟拉高 11 位
PS2_SET_SDA_IN();//SDA in
Wait_PS2_Scl(0);//等待时钟拉低
if(PS2_SDA==0)Wait_PS2_Scl(1);//等待时钟拉高 12 位
else
{
PS2_En_Data_Report();
return 1;//发送失败
}
}else
{
PS2_En_Data_Report();
return 2;//发送失败
}
PS2_En_Data_Report();
return 0; //发送成功
}
//PS2 初始化
//CHECK OK 2017/6/13
void PS2_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOA_CLK_ENABLE();
//开启 GPIOA 时钟
__HAL_RCC_GPIOC_CLK_ENABLE();
//开启 GPIOC 时钟
GPIO_Initure.Pin=GPIO_PIN_15;
//PA15
GPIO_Initure.Mode=GPIO_MODE_IT_RISING; //上升沿
GPIO_Initure.Pull=GPIO_PULLUP;
//上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH; //高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
GPIO_Initure.Pin=GPIO_PIN_5;
//PC5
GPIO_Initure.Mode=GPIO_MODE_INPUT;
//输入
HAL_GPIO_Init(GPIOC,&GPIO_Initure);
HAL_NVIC_SetPriority(EXTI15_10_IRQn,2,1); //抢占优先级为 2,子优先级为 1
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn); //使能中断线 15
}
该部分为底层的 PS/2 协议驱动程序,采用中断接收 PS/2 设备产生的时钟信号,然后解析。
保存 ps2.c 文件,并加入到 HARDWARE 组下,然后打开 ps2.h,在该文件里面输入如下代
码:
#ifndef __PS2_H
#define __PS2_H
#include "delay.h"
#include "sys.h"
#define PS2_SCL PAin(15)
//PA15
#define PS2_SDA PCin(5)
//PC5
//PS2 输出
#define PS2_SCL_OUT PAout(15)
//PA15
#define PS2_SDA_OUT PCout(5)
//PC5
//设置 PS2_SCL 输入输出状态.
#define PS2_SET_SCL_IN() {GPIOA->CRH&=0X0FFFFFFF;GPIOA->CRH|=0X80000000;}
#define PS2_SET_SCL_OUT() {GPIOA->CRH&=0X0FFFFFFF;GPIOA->CRH|=0X30000000;}
//设置 PS2_SDA 输入输出状态.
#define PS2_SET_SDA_IN() {GPIOC->CRL&=0XFF0FFFFF;GPIOC->CRL|=0X00800000;}
#define PS2_SET_SDA_OUT() {GPIOC->CRL&=0XFF0FFFFF;GPIOC->CRL|=0X00300000;}
#define MOUSE 0X20 //鼠标模式
#define KEYBOARD 0X10 //键盘模式
#define CMDMODE 0X00 //发送命令
//PS2_Status 当前状态标志
//[5:4]:当前工作的模式;[7]:接收到一次数据
//[6]:校验错误;[3:0]:收到的数据长度;
extern u8 PS2_Status;
//定义为命令模式
extern u8 PS2_DATA_BUF[16]; //ps2 数据缓存区
extern u8 MOUSE_ID;
void PS2_Init(void);
u8 PS2_Send_Cmd(u8 cmd);
void PS2_Set_Int(u8 en);
u8 PS2_Get_Byte(void);
void PS2_En_Data_Report(void);
void PS2_Dis_Data_Report(void);
#endif
保存此部分代码,然后打开 mouse.c,输入如下代码:
#include "mouse.h"
#include "usart.h"
#include "lcd.h"
u8 MOUSE_ID;//用来标记鼠标 ID
PS2_Mouse MouseX;
//处理 MOUSE 的数据
void Mouse_Data_Pro(void)
{
MouseX.x_pos+=(signed char)PS2_DATA_BUF[1];
MouseX.y_pos+=(signed char)PS2_DATA_BUF[2];
MouseX.z_pos+=(signed char)PS2_DATA_BUF[3];
MouseX.bt_mask=PS2_DATA_BUF[0]&0X07;//取出掩码
}
//初始化鼠标
//返回:0,初始化成功
//其他:错误代码
//CHECK OK 2010/5/2
u8 Init_Mouse(void)
{
u8 t;
PS2_Init();
delay_ms(800);
//等待上电复位完成
PS2_Status=CMDMODE; //进入命令模式
t=PS2_Send_Cmd(PS_RESET); //复位鼠标
if(t!=0)return 1;
t=PS2_Get_Byte();
if(t!=0XFA)return 2;
t=0;
while((PS2_Status&0x80)==0)//等待复位完毕
{
t++; delay_ms(10);
if(t>50)return 3;
}
PS2_Get_Byte();//得到 0XAA
PS2_Get_Byte();//得到 ID 0X00
//进入滚轮模式的特殊初始化序列
PS2_Send_Cmd(SET_SAMPLE_RATE); //进入设置采样率
if(PS2_Get_Byte()!=0XFA)return 4;
//传输失败
PS2_Send_Cmd(0XC8);
//采样率 200
if(PS2_Get_Byte()!=0XFA)return 5;
//传输失败
PS2_Send_Cmd(SET_SAMPLE_RATE); //进入设置采样率
if(PS2_Get_Byte()!=0XFA)return 6;
//传输失败
PS2_Send_Cmd(0X64);
//采样率 100
if(PS2_Get_Byte()!=0XFA)return 7;
//传输失败
PS2_Send_Cmd(SET_SAMPLE_RATE); //进入设置采样率
if(PS2_Get_Byte()!=0XFA)return 8;
//传输失败
PS2_Send_Cmd(0X50);
//采样率 80
if(PS2_Get_Byte()!=0XFA)return 9;
//传输失败
//序列完成
PS2_Send_Cmd(GET_DEVICE_ID);
//读取 ID
if(PS2_Get_Byte()!=0XFA)return 10;
//传输失败
MOUSE_ID=PS2_Get_Byte();
//得到 MOUSE ID
PS2_Send_Cmd(SET_SAMPLE_RATE); //再次进入设置采样率
if(PS2_Get_Byte()!=0XFA)return 11;
//传输失败
PS2_Send_Cmd(0X0A);
//采样率 10
if(PS2_Get_Byte()!=0XFA)return 12;
//传输失败
PS2_Send_Cmd(GET_DEVICE_ID);
//读取 ID
if(PS2_Get_Byte()!=0XFA)return 13;
//传输失败
MOUSE_ID=PS2_Get_Byte();
//得到 MOUSE ID
PS2_Send_Cmd(SET_RESOLUTION); //设置分辨率
if(PS2_Get_Byte()!=0XFA)return 14;
//传输失败
PS2_Send_Cmd(0X03);
//8 点/mm
if(PS2_Get_Byte()!=0XFA)return 15;
//传输失败
PS2_Send_Cmd(SET_SCALING11);
//设置缩放比率为 1:1
if(PS2_Get_Byte()!=0XFA)return 16;
//传输失败
PS2_Send_Cmd(SET_SAMPLE_RATE); //设置采样率
if(PS2_Get_Byte()!=0XFA)return 17;
//传输失败
PS2_Send_Cmd(0X28);
//40
if(PS2_Get_Byte()!=0XFA)return 18;
//传输失败
PS2_Send_Cmd(EN_DATA_REPORT); //使能数据报告
if(PS2_Get_Byte()!=0XFA)return 19;
//传输失败
PS2_Status=MOUSE;
//进入鼠标模式
return 0;//无错误,初始化成功
}
该部分仅 2 个函数,Init_Mouse 用于初始化鼠标,让鼠标进入 Intellimouse 模式,里面的初
始化序列完全按照《PS/2 技术参考》里面介绍的来设计。另外一个函数就是将收到的数据简单
处理一下。保存 mouse.c,然后打开 mouse.h,输入如下内容:
#ifndef __MOUSE_H
#define __MOUSE_H
#include "ps2.h"
//HOST->DEVICE 的命令集
#define PS_RESET 0XFF //复位命令 回应 0XFA
……//省略部分指令
//#define RESEND
0XFE //再次发送
//鼠标结构体
typedef struct
{
short x_pos; //横坐标
short y_pos; //纵坐标
short z_pos; //滚轮坐标
u8 bt_mask; //按键标识,bit2 中间键;bit1,右键;bit0,左键
} PS2_Mouse;
extern PS2_Mouse MouseX;
extern u8 MOUSE_ID; //鼠标 ID,0X00,表示标准鼠标(3 字节);0X03 表示扩展鼠标(4 字节)
u8 Init_Mouse(void);
void Mouse_Data_Pro(void);
#endif
该部分代码定义了一个鼠标结构体,用于存放鼠标相关的数据,并对鼠标的相关命令进行
了宏定义(部分被省略),保存此部分代码。最后,打开 main.c 文件,修改代码如下:
//显示鼠标的坐标值
//x,y:在 LCD 上显示的坐标位置
//pos:坐标值
void Mouse_Show_Pos(u16 x,u16 y,short pos)
{
if(pos<0)
{
LCD_ShowChar(x,y,'-',16,0);
//显示负号
pos=-pos;
//转为正数
}else LCD_ShowChar(x,y,' ',16,0);
//去掉负号
LCD_ShowNum(x+8,y,pos,5,16);
//显示值
}
int main(void)
{
u8 t; u8 errcnt=0;
HAL_Init();
//初始化 HAL 库
Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M
delay_init(72);
//初始化延时函数
uart_init(115200);
//初始化串口
LED_Init();
//初始化 LED
LCD_Init();
//初始化 LCD
POINT_COLOR=RED;
//设置字体为红色
LCD_ShowString(60,50,200,16,16,"Mini STM32");
LCD_ShowString(60,70,200,16,16,"Mouse TEST");
LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(60,110,200,16,16,"2019/11/15");
while(Init_Mouse())
//检查鼠标是否在位.
{
LCD_ShowString(60,130,200,16,16,"Mouse Error");
delay_ms(400);
LCD_Fill(60,130,239,130+16,WHITE);
delay_ms(100);
}
LCD_ShowString(60,130,200,16,16,"Mouse OK");
LCD_ShowString(60,150,200,16,16,"Mouse ID:");
LCD_ShowNum(132,150,MOUSE_ID,3,16);//填充模式
POINT_COLOR=BLUE;
LCD_ShowString(30,170,200,16,16,"BUF[0]:");
LCD_ShowString(30,186,200,16,16,"BUF[1]:");
LCD_ShowString(30,202,200,16,16,"BUF[2]:");
if(MOUSE_ID==3)LCD_ShowString(30,218,200,16,16,"BUF[3]:");
LCD_ShowString(90+30,170,200,16,16,"X POS:");
LCD_ShowString(90+30,186,200,16,16,"Y POS:");
LCD_ShowString(90+30,202,200,16,16,"Z POS:");
if(MOUSE_ID==3)LCD_ShowString(90+30,218,200,16,16,"BUTTON:");
t=0;
while(1)
{
if(PS2_Status&0x80)//得到了一次数据
{
LCD_ShowNum(56+30,170,PS2_DATA_BUF[0],3,16);//填充模式
LCD_ShowNum(56+30,186,PS2_DATA_BUF[1],3,16);//填充模式
LCD_ShowNum(56+30,202,PS2_DATA_BUF[2],3,16);//填充模式
if(MOUSE_ID==3)LCD_ShowNum(56+30,218,PS2_DATA_BUF[3],3,16);
//填充模式
Mouse_Data_Pro();//处理数据
Mouse_Show_Pos(146+30,170,MouseX.x_pos);
//X 坐标
Mouse_Show_Pos(146+30,186,MouseX.y_pos);
//Y 坐标
if(MOUSE_ID==3)Mouse_Show_Pos(146+30,202,MouseX.z_pos); //滚轮位置
if(MouseX.bt_mask&0x01)LCD_ShowString(146+30,218,200,16,16,"LEFT");
else LCD_ShowString(146+30,218,200,16,16," ");
if(MouseX.bt_mask&0x02)LCD_ShowString(146+30,234,200,16,16,"RIGHT");
else LCD_ShowString(146+30,234,200,16,16," ");
if(MouseX.bt_mask&0x04)LCD_ShowString(146+30,250,200,16,16,"MIDDLE");
else LCD_ShowString(146+30,250,200,16,16," ");
PS2_Status=MOUSE;
PS2_En_Data_Report();//使能数据报告
}else if(PS2_Status&0x40)
{
errcnt++;
PS2_Status=MOUSE;
LCD_ShowNum(86+30,234,errcnt,3,16);//填充模式
}
t++;
delay_ms(1);
if(t==200)
{
t=0;
LED0=!LED0;
}
}
}
此部分,除了 main 函数,我们还编写了 Mouse_Show_Pos 函数,用于在指定位置显示鼠标坐标值,并支持负数显示,通过该函数,可以方便我们显示鼠标坐标数据。至此,PS/2 鼠标实验的软件设计部分就结束了。
30.4 下载验证
在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到
LCD 显示如图 30.4.1 所示内容(假定 PS/2 鼠标已经接上,并且初始化成功):
图 30.4.1 PS/2 鼠标实验显示结果
移动鼠标,或者按动按键,就可以看到上面的数据不断变化,证明我们的鼠标已经成功被
驱动了,接下来我们就可以使用鼠标来控制 STM32 了。
几款主流单片机优缺点对比
单片机现在可谓是铺天盖地,种类繁多,让开发者们应接不暇,发展也是相当的迅速,从上世纪80年代,由当时的4位8位发展到现在的各种高速单片机。
各个厂商们也在速度、内存、功能上此起彼伏,参差不齐。同时涌现出一大批拥有代表性单片机的厂商:Atmel、TI、ST、MicroChip、ARM…… 除此之外国内厂商的STC单片机也是可圈可点。
下面为大家带来51、MSP430、STM32、TMS、PIC、AVR、STC单片机之间的优缺点比较及功能体现。
51单片机
应用最广泛的8位单片机当然也是初学者们最容易上手学习的单片机,最早由Intel推出,由于其典型的结构和完善的总线专用寄存器的集中管理,众多的逻辑位操作功能及面向控制的丰富的指令系统,堪称为一代“经典”,为以后的其它单片机的发展奠定了基础。目前在教学场合和对性能要求不高的场合大量被采用。
特点
51单片机之所以成为经典,成为易上手的单片机主要有以下特点:
从内部的硬件到软件有一套完整的按位操作系统,称作位处理器,处理对象不是字或字节而是位。不但能对片内某些特殊功能寄存器的某位进行处理,如传送、置位、清零、测试等,还能进行位的逻辑运算,其功能十分完备,使用起来得心应手。同时在片内RAM区间还特别开辟了一个双重功能的地址区间,使用极为灵活,这一功能无疑给使用者提供了极大的方便。乘法和除法指令,这给编程也带来了便利。很多的八位单片机都不具备乘法功能,做乘法时还得编上一段子程序调用,十分不便。缺点
51单片机虽然是经典但是缺点还是很明显的
AD、EEPROM等功能需要靠扩展,增加了硬件和软件负担。虽然I/O脚使用简单,但高电平时无输出能力,这也是51系列单片机的最大软肋。运行速度过慢,特别是双数据指针,如能改进能给编程带来很大的便利。51保护能力很差,很容易烧坏芯片。MSP430单片机
MSP430系列单片机是1996年开始推向市场的一种16位超低功耗的混合信号处理器,给人们留下的最大的亮点是低功耗而且速度快,汇编语言用起来很灵活,寻址方式很多,指令很少,容易上手。主要是由于其针对实际应用需求,把许多模拟电路、数字电路和微处理器集成在一个芯片上,以提供“单片”解决方案。在低功耗及超低功耗的工业场合应用的比较多。
特点
MSP430单片机其迅速发展和应用范围的不断扩大,主要取决于以下的特点。
强大的处理能力,采用了精简指令集(RISC)结构,具有丰富的寻址方式( 7 种源操作数寻址、 4 种目的操作数寻址)、简洁的 27 条内核指令以及大量的模拟指令;大量的寄存器以及片内数据存储器都可参加多种运算;还有高效的查表处理指令;有较高的处理速度,在 8MHz 晶体驱动下指令周期为 125 ns 。这些特点保证了可编制出高效率的源程序。在运算速度方面,能在 8MHz 晶体的驱动下,实现 125ns 的指令周期;16 位的数据宽度、 125ns 的指令周期以及多功能的硬件乘法器(能实现乘加)相配合,能实现数字信号处理的某些算法(如 FFT 等)。超低功耗方面,MSP430 单片机之所以有超低的功耗,是因为其在降低芯片的电源电压及灵活而可控的运行时钟方面都有其独到之处;电源电压采用的是 1.8~3.6V 电压,因而可使其在 1MHz 的时钟条件下运行时, 芯片的电流会在 200~400uA 左右,时钟关断模式的最低功耗只有 0.1uA。缺点
可能不太容易上手,不适合初学者入门,资料也比较少,只能跑官网去找。占的指令空间较大,因为是16位单片机,程序以字为单位,有的指令竟然占6个字节,虽然程序表面上简洁, 但与pic单片机比较空间占用很大。STM32单片机
由ST厂商推出的STM32系列单片机,行业的朋友都知道,这是一款性价比超高的系列单片机,应该没有之一,功能及其强大。其基于专为要求高性能、低成本、低功耗的嵌入式应用专门设计的ARM Cortex-M内核,同时具有一流的外设:1μs的双12位ADC,4兆位/秒的UART,18兆位/秒的SPI等等,在功耗和集成度方面也有不俗的表现,当然和MSP430的功耗比起来是稍微逊色的一些,但这并不影响工程师们对它的热捧程度,由于其简单的结构和易用的工具再配合其强大的功能在行业中赫赫有名。
特点
STM32单片机其强大的功能主要表现在:
内核:ARM32位Cortex-M3CPU,最高工作频率72MHz,1.25DMIPS/MHz,单周期乘法和硬件除法。存储器:片上集成32-512KB的Flash存储器;6-64KB的SRAM存储器。时钟、复位和电源管理:2.0-3.6V的电源供电和I/O接口的驱动电压;POR、PDR和可编程的电压探测器(PVD);4-16MHz的晶振;内嵌出厂前调校的8MHz RC振荡电路,内部40 kHz的RC振荡电路;用于CPU时钟的PLL;带校准用于RTC的32kHz的晶振。调试模式:串行调试(SWD)和JTAG接口;最多高达112个的快速I/O端口、最多多达11个定时器、最多多达13个通信接口。TMS单片机
这里也提一下TMS系列单片机,虽不算主流。由TI推出的8位CMOS单片机,具有多种存储模式、多种外围接口模式,适用于复杂的实时控制场合。虽然没STM32那么优秀,也没MSP430那么张扬,但是TMS370C系列单片机提供了通过整合先进的外围功能模块及各种芯片的内存配置,具有高性价比的实时系统控制。同时采用高性能硅栅CMOS EPROM和EEPROM技术实现。低工作功耗CMOS技术,宽工作温度范围,噪声抑制,再加上高性能和丰富的片上外设功能,使TMS370C系列单片机在汽车电子,工业电机控制,电脑,通信和消费类具有一定的应用。
PIC单片机
PIC单片机系列是美国微芯公司(Microship)的产品,共分三个级别,即基本级、中级、高级,是当前市场份额增长最快的单片机之一,CPU采用RISC结构,分别有33、35、58条指令,属精简指令集。
同时采用Harvard双总线结构,运行速度快,它能使程序存储器的访问和数据存储器的访问并行处理,这种指令流水线结构,在一个周期内完成两部分工作,一是执行指令,二是从程序存储器取出下一条指令,这样总的看来每条指令只需一个周期,这也是高效率运行的原因之一。
特点
PIC单片机之所以成为一时非常热的单片机不外乎以下特点:
具有低工作电压、低功耗、驱动能力强等特点。PIC系列单片机的I/O口是双向的,其输出电路为CMOS互补推挽输出电路,I/O脚增加了用于设置输入或输出状态的方向寄存器,从而解决了51系列I/O脚为高电平时同为输入和输出的状态。当置位1时为输入状态,且不管该脚呈高电平或低电平,对外均呈高阻状态;置位0时为输出状态,不管该脚为何种电平,均呈低阻状态,有相当的驱动能力,低电平吸入电流达25mA,高电平输出电流可达20mA。相对于51系列而言,这是一个很大的优点。它可以直接驱动数码管显示且外电路简单。它的A/D为10位,能满足精度要求。具有在线调试及编程(ISP)功能。缺点
其专用寄存器(SFR)并不像51系列那样都集中在一个固定的地址区间内(80~FFH),而是分散在四个地址区间内。只有5个专用寄存器PCL、STATUS、FSR、PCLATH、INTCON在4个存储体内同时出现,但是在编程过程中,少不了要与专用寄存器打交道,得反复地选择对应的存储体,也即对状态寄存器STATUS的第6位(RP1)和第5位(RP0)置位或清零。数据的传送和逻辑运算基本上都得通过工作寄存器W(相当于51系列的累加器A)来进行,而51系列的还可以通过寄存器相互之间直接传送,因而PIC单片机的瓶颈现象比51系列还要严重,这在编程中的朋友应该深有体会。AVR单片机
AVR单片机是Atmel公司推出的较为新颖的单片机,其显著的特点为高性能、高速度、低功耗。它取消机器周期,以时钟周期为指令周期,实行流水作业。AVR单片机指令以字为单位,且大部分指令都为单周期指令。而单周期既可执行本指令功能,同时完成下一条指令的读取。通常时钟频率用4~8MHz,故最短指令执行时间为250~125ns。
特点
AVR单片机能成为最近仍是比较火热的单片机,主要的特点:
AVR系列没有类似累加器A的结构,它主要是通过R16~R31寄存器来实现A的功能。在AVR中,没有像51系列的数据指针DPTR,而是由X(由R26、R27组成)、Y(由R28、R29组成)、Z(由R30、R31组成)三个16位的寄存器来完成数据指针的功能(相当于有三组DPTR),而且还能作后增量或先减量等的运行,而在51系列中,所有的逻辑运算都必须在A中进行;而AVR却可以在任两个寄存器之间进行,省去了在A中的来回折腾,这些都比51系列出色些。AVR的专用寄存器集中在00~3F地址区间,无需像PIC那样得先进行选存储体的过程,使用起来比PIC方便。AVR的片内RAM的地址区间为0~00DF(AT90S2313) 和0060~025F(AT90S8515、AT90S8535),它们占用的是数据空间的地址,这些片内RAM仅仅是用来存储数据的,通常不具备通用寄存器的功能。当程序复杂时,通用寄存器R0~R31就显得不够用;而51系列的通用寄存器多达128个(为AVR的4倍),编程时就不会有这种感觉。AVR的I/O脚类似PIC,它也有用来控制输入或输出的方向寄存器,在输出状态下,高电平输出的电流在10mA左右,低电平吸入电流20mA。这点虽不如PIC,但比51系列还是要优秀的。缺点
是没有位操作,都是以字节形式来控制和判断相关寄存器位的。C语言与51的C语言在写法上存在很大的差异,这让从开始学习51单片机的朋友很不习惯。通用寄存器一共32个(R0~R31),前16个寄存器(R0~R15)都不能直接与立即数打交道,因而通用性有所下降。而在51系列中,它所有的通用寄存器(地址00~7FH)均可以直接与立即数打交道,显然要优于前者。Freescale单片机
主要针对S08,S12这类单片机,当然Freescale单片机远非于此。Freescale系列单片机采用哈佛结构和流水线指令结构,在许多领域内都表现出低成本,高性能的的特点,它的体系结构为产品的开发节省了大量时间。此外Freescale提供了多种集成模块和总线接口,可以在不同的系统中更灵活的发挥作用。
特点
Freescale单片机的特有的特点如下:
全系列:从低端到高端,从8位到32位全系列应有尽有,其推出的8位/32位管脚兼容的QE128,可以从8位直接移植到32位,弥补单片机业界8/32 位兼容架构中缺失的一环。多种系统时钟模块:三种模块,七种工作模式。多种时钟源输入选项,不同的mcu具有不同的时钟产生机制,可以是RC振荡器,外部时钟或晶振,也可以是内部时钟,多数CPU同时具有上述三种模块;可以运行在FEI,FEE,FBI,FBILP,FBE,FBELP,STOP这七种工作模式。多种通讯模块接口:Freescale单片机几乎在内部集成各种通信接口模块:包括串行通信接口模块SCI,多主I2C总线模块,串行外围接口模块 SPI,MSCAN08控制器模块,通用串行总线模块(USB/PS2)。具有更多的可选模块:具有LCD驱动模块,带有温度传感器,具有超高频发送模块,含有同步处理器模块,含有同步处理器的MCU还具有屏幕显示模块OSD,还有少数的MCU具有响铃检测模块RING和双音多频/音调发生器DMG模块。可靠性高,抗干扰性强,多种引脚数和封装选择。低功耗、也许Freescale系列的单片机的功耗没有MSP430的低,但是他具有全静态的“等待”和“停止”两种模式,从总体上降低您的功耗!新近推出的几款超低功耗已经与MSP430的不相上下。STC单片机
说到STC单片机有人会说到,STC也能算主流?基于它是国内还算是比较不错的单片机来说。STC是单时钟/机器周期的单片机,说白了STC单片机是51与AVR的结合体,有人说AVR是51的替代单片机,但是AVR单片机在位控制和C语言写法上存在很大的差异。
而STC单片机结合了51和AVR的优点,虽然功能不及AVR那么强大,但是在AVR能找到的功能,在STC上基本都有,同时STC单片机是51内核,这给以51单片机为基础的工程师们提供了极大的方便,省去了学习AVR的时间,同时也不失AVR的各种功能。
STC单片机是高速、低功耗、超强抗干扰的新一代8051单片机51单片机,指令代码完全兼容传统8051,但速度快8~12倍,内部集成MAX810专用复位电路。4路PWM 8路高速10位A、D转换,针对电机电机 的供应商控制,强干扰场合,成为继51单片机后一个全新系列单片机。
特点
下载烧录程序用串口方便好用,容易上手,拥有大量的学习资料及视频,同时具有宽电压:5.5~3.8V、2.4~3.8V, 低功耗设计:空闲模式,掉电模式(可由外部中断唤醒)。STC单片机具有在应用编程,调试起来比较方便;带有10位AD,内部EEPROM,可在1T/机器周期下工作,速度是传统51单片机的8~12倍,价格也较便宜。4通道捕获/比较单元,STC12C2052AD系列为2通道,也可用来再实现4个定时器或4个外部中断,2个硬件16位定时器,兼容普通8051的定时器。4路PCA还可再实现4个定时器,具有硬件看门狗、高速SPI通信端口、全双工异步串行口,兼容普通8051的串口,同时还具有先进的指令集结构,兼容普通8051指令集。硬件笔记本,一起学习电路设计、PCB设计、仿真、调试以及EMC知识
更多干货文章请点击关注:
搞懂元器件,就搞懂了电路的一半
一起学习吧:
硬件笔记本|加群
声明:本号对所有原创、转载文章的陈述与观点均保持中立,推送文章仅供读者学习和交流。文章、图片等版权归原作者享有,如有侵权,联系删除。
相关问答
PS/2接口有哪些存在的意义呢? 当你按下A键时,电脑是怎么知道你按的是A键而不是其他键呢?