• 学习笔记|矩阵按键控制原理|数值转化为键码|密码锁|STC32G单片机视频开发教程(冲哥)|第十四集:矩阵按键原理及实践


    1.矩阵按键是什么

    这个矩阵按键也是我们这个开发版上最后一个GPIO的一个应用,如果对IO回的输入跟输出还有什么问题的话,一定要回过头去看一下我们之前的程序理清楚思路。
    之前的按键电路图:
    在这里插入图片描述
    1个按键占用一个IO口的。
    在按键数量较多时,为了减少I/O口的占用,将按键排列成矩阵排列的形式的按键阵列我们称位矩阵按键。

    2.矩阵按键的控制原理

    电路图:
    在这里插入图片描述

    按键识别原理:端口默认为高电平,实时读取到引脚为低电平是表示按下。再次读取到高电平,表示松开。

    第一步:现将P0.0-P0.3输出低电平,P0.6-P0.7输出高电平,如果有按键按下,按下的那一列的IO就会变成低电平,就可以判断出哪一列按下了。

    第二步:现将P0.0-P0.3输出高电平,P0.6-P0.7输出低电平,如果有按键按下,按下的那一行的IO就会变成低电平,就可以判断出哪一行按下了。

    第三步:行列组合一下就可以判断出是哪个按键按下了。

    按键按下后导线导通,哪条线上有高电平,就会被拉低为低电平,从而检测出是哪条线路,二次检查,交叉节点就是有按键按下的按键位置。

    3.矩阵按键程序的编写

    先完成矩阵按键的功能编写。
    复制9.TIM多任务为10.矩阵按键,用到P0端口,还是在之前的KEY模块基础上进行修改:
    在key.h中定义:#define MateixKEY P0 //矩阵按键的引脚
    定义函数MateixKEY_Read:
    u8 MateixKEY_Read(void); //矩阵按键读取当前是哪一个按钮按下,返回值是按键序号
    在key.c中实现函数MateixKEY_Read:
    先增加函数头,并把实现思路复制过来作为编写依据,围绕这三步,编写矩阵按键的读取程序:
    MateixKEY = 0XC0; //1100 0000 P0.6-P0.7输出高电平,
    在这里插入图片描述

    增加延时函数MateixKEY_delay留反应时间:
    MateixKEY_delay(); //留反应时间
    MateixKEY_delay的实现,并添加函数头:

    void MateixKEY_delay(void)
    {
    	u8 i;
    	i = 60; //根据之前的毫秒延时函数,可以算出此处延时的时间
    	while(--i);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    第一步:假设P0.7按下, 则为0100 0000,如果想实现哪路按下哪位变成1,可以采用异或运算。
    即:0100 0000 ^1100 0000 = 1000 0000
    则有:keystate = MateixKEY^0XC0;

    第二步:第二次扫描,高位输出低电平,低位输出高电平:MateixKEY = 0X0f; //0000 1111
    保存按键状态,假设P0.0按下, 则为0000 1110^0000 1111 = 0000 0001,这里要采用|=,0000 0001 | 1000 0000 = 1000 0001 = 0x81
    在这里插入图片描述
    keystate |= (MateixKEY^0X0f);是为了避免把之前的数值覆盖。
    第三步:keystate中已经保存了行、列的状态,行列组合一下就可以判断出是哪个按键按下了。
    printf(“%02x\r\n”,keystate); //强制变为2位,以16进制显示。
    return keystate;
    在demo.c中调用:
    将10ms扫描按键的代码部分注释掉,只检测MateixKEY_Read,加入该函数,编译,运行。按动按键,串口打印对应的16进制数值。

    将数值转化为键码

    u8 key_val = 0; //表示按键的键码
    这里采用switch关键词,直接有模板插入,编写switch函数:

    	switch (keystate) //单选开关函数
        {
        	case 0x41:	key_val = 1;
        		break;
        	case 0x42:	key_val = 2;
        		break;
        	case 0x44:	key_val = 3;
        		break;
        	case 0x48:	key_val = 4;
        		break;
        	case 0x81:	key_val = 5;
        		break;
        	case 0x82:	key_val = 6;
        		break;
        	case 0x84:	key_val = 7;
        		break;
        	case 0x88:	key_val = 8;
        		break;
        	default:  	key_val = 0;
        		break;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    关闭数码管初始化显示,main函数中新建变量:u8 KEY_NUM = 0; //保存矩阵按键的键码
    读取矩阵按键的键码保存在KEY_NUM中,并在数码管最后1位显示:

    			KEY_NUM = MateixKEY_Read();
    			SEG7 = KEY_NUM; 	//在数码管最后一位显示
    
    • 1
    • 2

    编译下载,按动按键就可以判断是哪个按键按下了。

    完整代码:

    demo.c:

    #include "COMM/stc.h"		//调用头文件
    #include "COMM/usb.h"
    #include "seg_led.h"
    #include "key.h"			//调用头文件
    #include "beep.h"
    #include "tim0.h"
    
    
    
    #define MAIN_Fosc 24000000UL	//定义主时钟
    
    char *USER_DEVICEDESC = NULL;
    char *USER_PRODUCTDESC = NULL;
    char *USER_STCISPCMD = "@STCISP#";
    
    bit TIM_10MS_Flag;		//10ms标志位
    
    void sys_init();	//函数声明
    void delay_ms(u16 ms);
    
    void Timer0_Isr(void);
    
    
    void main()					//程序开始运行的入口
    {
    	u8 KEY_NUM = 0; 		//保存矩阵按键的键码
    	sys_init();				//USB功能+IO口初始化
    	usb_init();				//usb库初始化
    	Timer0_Init();
    
    	EA = 1;					//CPU开放中断,打开总中断。
    
    
    	//数码管初始化,显示0-7
    //	SEG0 = 0;
    //	SEG1 = 1;
    //	SEG2 = 2;
    //	SEG3 = 3;
    //	SEG4 = 4;
    //	SEG5 = 5;
    //	SEG6 = 6;
    //	SEG7 = 7;
    
    	LED = 0x0f;	//赋初值,亮一半灭一半,可以写8位的变量.从7开始数到0
    
    	while(1)		//死循环
    	{
    //		if( DeviceState != DEVSTATE_CONFIGURED ) 	//
    //			continue;
    		if( bUsbOutReady )
    		{
    			usb_OUT_done();
    		}
    		if(TIM_10MS_Flag == 1)   //将需要延时的代码部分放入
    		{
    			TIM_10MS_Flag = 0;		//TIM_10MS_Flag 变量清空置位
    //			KEY_Deal();				//P3上所有端口都需要执行一遍
    			BEEP_RUN();				//蜂鸣运行
    
    //			if(KEY_ReadState(KEY1)== KEY_RESS)	//判断KEY1按钮是否为单击
    //			{
    //				BEEP_ON(2);							//蜂鸣20ms
    //				LED0 = 0;
    //			}
    //			else if(KEY_ReadState(KEY1)== KEY_LONGPRESS) //判断KEY1按钮是否为长按
    //			{
    //				BEEP_ON(2);							//蜂鸣20ms
    //				LED1 = 0;
    //			}
    //			else if(KEY_ReadState(KEY1)== KEY_RELAX)	//判断KEY1按钮是否为松开
    //			{
    //				LED = 0XFF;
    //			}
    			KEY_NUM = MateixKEY_Read();
    			SEG7 = KEY_NUM; 	//在数码管最后一位显示
    		}
    
    	}
    }
    
    void sys_init()		//函数定义
    {
        WTST = 0;  //设置程序指令延时参数,赋值为0可将CPU执行指令的速度设置为最快
        EAXFR = 1; //扩展寄存器(XFR)访问使能
        CKCON = 0; //提高访问XRAM速度
    
    	P0M1 = 0x00;   P0M0 = 0x00;   //设置为准双向口
        P1M1 = 0x00;   P1M0 = 0x00;   //设置为准双向口
        P2M1 = 0x00;   P2M0 = 0x00;   //设置为准双向口
        P3M1 = 0x00;   P3M0 = 0x00;   //设置为准双向口
        P4M1 = 0x00;   P4M0 = 0x00;   //设置为准双向口
        P5M1 = 0x00;   P5M0 = 0x00;   //设置为准双向口
        P6M1 = 0x00;   P6M0 = 0x00;   //设置为准双向口
        P7M1 = 0x00;   P7M0 = 0x00;   //设置为准双向口
    
        P3M0 = 0x00;
        P3M1 = 0x00;
    
        P3M0 &= ~0x03;
        P3M1 |= 0x03;
    
        //设置USB使用的时钟源
        IRC48MCR = 0x80;    //使能内部48M高速IRC
        while (!(IRC48MCR & 0x01));  //等待时钟稳定
    
        USBCLK = 0x00;	//使用CDC功能需要使用这两行,HID功能禁用这两行。
        USBCON = 0x90;
    }
    
    
    void delay_ms(u16 ms)	//unsigned int
    {
    	u16 i;
    	do
    	{
    		i = MAIN_Fosc/6000;
    		while(--i);
    	}while(--ms);
    }
    
    void Timer0_Isr(void) interrupt 1 //1ms进来执行一次,无需其他延时,重复赋值
    {
    	static timecount = 0;
    
    	SEG_LED_Show();		//数码管刷新
    
    	timecount++;		//1ms+1
    	if(timecount>=10)	//如果这个变量大于等于10,说明10ms到达
    	{
    		timecount = 0;
    		TIM_10MS_Flag = 1;	//10ms到了
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133

    key.c:

    #include "key.h"			//调用头文件
    u16 Count[8] = {0,0,0,0,0,0,0,0};	//按键的时间状态变量初始化8位
    u8 LastState = 0;					//8位变量,b0=1 则表示key0上一次按下过
    
    
    //========================================================================
    // 函数名称:KEY_Deal
    // 函数功能:按键状态的获取
    // 入口参数:无
    // 函数返回:无
    // 当前版本: VER1.0
    // 修改日期: 2023-1-1
    // 当前作者:
    // 其他备注:循环读取8个端口的状态,并将按下的时间赋值给Count数组,然后按下的状态赋值给变量LastState
    //========================================================================
    void KEY_Deal(void) 			//检查所有的按键状态,10ms执行一次
    {
    	u8 i = 0;
    	for(i=0;i<8;i++)			//for循环变量 循环8次,i取值为0-7,代表P30-P37的状态查询
    	{
    		if(~KEY & (1< 0 )	//如果这个按键是按下过的,
    			{
    				LastState |= (1< 0)			//判断按键是按下的
    	{
    		if(Count[keynum] < 3)		//按下小于30ms,返回消抖状态
    		{
    			return KEY_FLCKER;
    		}
    		else if(Count[keynum] == 3)	//按正好等于30ms,返回单击状态
    		{
    			return KEY_RESS;
    		}
    		else if(Count[keynum] < 300 ) //按下小于3000ms,返回单击结束
    		{
    			return KEY_PRESSOVER;
    		}
    		else if(Count[keynum] == 300 ) //按下正好等于3000ms,返回长按
    		{
    			return KEY_LONGPRESS;
    		}
    		else					//长按结束
    		{
    			return KEY_LONGOVER;
    		}
    	}
    	else						//按键已经松开了,返回KEY_RELAX状态
    	{
    		if(LastState &(1<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165

    key.h:

    #ifndef __KEY_H
    #define __KEY_H
    
    #include "COMM/stc.h"			//调用头文件
    #include "COMM/usb.h"
    
    //------------------------引脚定义------------------------//
    #define KEY P3 			//定义一个按键 引脚选择P32-P36
    
    #define KEY1 2			//按键1
    #define KEY2 3			//按键2
    #define KEY3 4			//按键3
    #define KEY4 5			//按键4
    
    #define MateixKEY P0    //矩阵按键的引脚
    
    
    //------------------------变量声明------------------------//
    //状态	功能
    #define KEY_NOPRESS 0 		//按键未按下	0
    #define KEY_FLCKER 1 		//消抖	1
    #define KEY_RESS 2			//单击	2
    #define KEY_PRESSOVER 3 	//单击结束	3
    #define KEY_LONGPRESS 4 	//长按3s	4
    #define KEY_LONGOVER 5 		//长按结束	5
    #define KEY_RELAX 6 		//按键松开	6
    
    
    //------------------------函数声明-----------------------//
    void KEY_Deal(void);			//检查所有的按键状态
    u8  KEY_ReadState(u8 keynum);	//读取指定按键的状态
    
    u8 MateixKEY_Read(void);		//矩阵按键读取当前是哪一个按钮按下,返回值是按键序号
    
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    密码锁(简易版)

    在这里插入图片描述

    由于KEY_NUM = MateixKEY_Read();执行后,会持续输出1,需要修改,3s内只输出1次即可,让它符合今天的主题。
    先定义一个静态变量,这是一个很常见的用法,static u8 keystate_Last; //表示当前的按钮上一次的状态值
    增加判断条件:

    	if(keystate_Last != keystate)	//如果本次获取到的按键状态值和之前的不一样
    	{
    		keystate_Last = keystate;   //把本次的按键状态值写入进去
    
    		switch (keystate) //单选开关函数
    		{
    			case 0x41:	key_val = 1;
    				break;
    			case 0x42:	key_val = 2;
    				break;
    			case 0x44:	key_val = 3;
    				break;
    			case 0x48:	key_val = 4;
    				break;
    			case 0x81:	key_val = 5;
    				break;
    			case 0x82:	key_val = 6;
    				break;
    			case 0x84:	key_val = 7;
    				break;
    			case 0x88:	key_val = 8;
    				break;
    			default:  	key_val = 0;
    				break;
    		}
    		printf("%d\r\n",(int)key_val);  //强制转化为整形变量
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    需求分析:

    1.通过LED0模拟门锁状态,LED点亮表示门锁打开,熄灭表示门锁锁上;
    增加横线显示值,SEG_Tab[22]=
    默认显示横线:u8 Show_Tab[8] = {21,21,21,21,21,21,21,21};
    新增变量:u8 KEY_Str = 0; //表示当前输入了几个密码
    2.增加8位数码管,可以动态显示8位的密码,无密码时显示 “- - - - - - - -”;
    3.通过矩阵按键可以输入1-8的数字表示密码,并依次显示在数码管上;
    4.每输入一个数字,蜂鸣器响20ms表示有数字按下;
    5.密码正确打开LED0,密码错误蜂鸣响2秒;
    根据条件,修改demo.c中的main函数代码如下:

    void main()					//程序开始运行的入口
    {
    	u8 KEY_NUM = 0; 		//保存矩阵按键的键码
    	u8 KEY_Str = 0; 		//表示当前输入了几个密码位
    	sys_init();				//USB功能+IO口初始化
    	usb_init();				//usb库初始化
    	Timer0_Init();
    
    	EA = 1;					//CPU开放中断,打开总中断。
    
    
    	//数码管初始化,显示0-7
    //	SEG0 = 0;
    //	SEG1 = 1;
    //	SEG2 = 2;
    //	SEG3 = 3;
    //	SEG4 = 4;
    //	SEG5 = 5;
    //	SEG6 = 6;
    //	SEG7 = 7;
    
    	//LED = 0x0f;	//赋初值,亮一半灭一半,可以写8位的变量.从7开始数到0
    	LED = 0xff;	//赋初值,密码锁应用初始状态熄灭所有LED
    	while(1)		//死循环
    	{
    //		if( DeviceState != DEVSTATE_CONFIGURED ) 	//
    //			continue;
    		if( bUsbOutReady )
    		{
    			usb_OUT_done();
    		}
    		if(TIM_10MS_Flag == 1)   //将需要延时的代码部分放入
    		{
    			TIM_10MS_Flag = 0;		//TIM_10MS_Flag 变量清空置位
    //			KEY_Deal();				//P3上所有端口都需要执行一遍
    			BEEP_RUN();				//蜂鸣运行
    
    //			if(KEY_ReadState(KEY1)== KEY_RESS)	//判断KEY1按钮是否为单击
    //			{
    //				BEEP_ON(2);							//蜂鸣20ms
    //				LED0 = 0;
    //			}
    //			else if(KEY_ReadState(KEY1)== KEY_LONGPRESS) //判断KEY1按钮是否为长按
    //			{
    //				BEEP_ON(2);							//蜂鸣20ms
    //				LED1 = 0;
    //			}
    //			else if(KEY_ReadState(KEY1)== KEY_RELAX)	//判断KEY1按钮是否为松开
    //			{
    //				LED = 0XFF;
    //			}
    			KEY_NUM = MateixKEY_Read();		//当前矩阵按键的键值
    			//SEG7 = KEY_NUM; 	//在数码管最后一位显示
    			if( KEY_NUM > 0)				//如果有按键按下
    			{
    				KEY_NUM = 0;						//键值先清空,清空按键
    				BEEP_ON(2);							//蜂鸣20ms
    				Show_Tab[KEY_Str] = KEY_NUM; 		//表示当前输入了几个密码 = KEY_NUM;		//将当前的按键状态保存到数组
    				KEY_Str++;							//输入的密码位数+1
    
    				if(KEY_Str ==8)		//如果密码已经等于8位,
    				{
    					if((Show_Tab[0]==1)&&(Show_Tab[1]==1)&&(Show_Tab[2]==1)&&(Show_Tab[3]==1)&&(Show_Tab[4]==1)&&(Show_Tab[5]==1)&&(Show_Tab[6]==1)&&(Show_Tab[7]==1))
    					{
    						LED0 = 0;			//如果密码正确,LED0点亮
    					}
    					else
    					{
    						BEEP_ON(200);	//密码错误,蜂鸣2s。单位是10ms,2000ms=2s
    					}
    				}
    			}
    		}
    
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    编译下载,发现按键后均显示0:KEY_NUM = 0;位置有误,应该放在调用以后再置0.
    重新测试,8位输入后不能自动清空,应该添加代码,到达长度后回到初始值显示横杠:SEG0 = SEG1 = SEG2 = SEG3 = SEG4 = SEG5 = SEG6 = SEG7 = 21;
    重新测试,输入错误后,复位,再按键输入,显示有问题:KEY_Str ==8后忘记归0了。
    经过修改,功能已正常实现。
    完整代码请参考:《STC单片机原理-教学视频配套附件-20230731.zip

    总结

    1.了解矩阵按键的工作原理和代码编写的过程

    课后练习:

    给今天的门锁增加如下功能:
    1.LED0(门锁)打开后,5秒后自动关闭;
    2.增加门内的手动开门按钮,按下按钮门锁打开;
    3.10秒内没有输入密码自动数码管熄灭省电;有按键按下时再显示。
    4.用for去改写一下密码判断的地方。

  • 相关阅读:
    华为云云耀云服务器L实例评测 | 实例使用教学之高级使用:配置 Git SSH Key 进行自动识别拉代码
    使用stelnet进行安全的远程管理
    在ubuntu20下使用花生壳映射vscode SSH
    【Java-LangChain:使用 ChatGPT API 搭建系统-11】用 ChatGPT API 构建系统 总结篇
    Worthington公司天冬氨酸氨基转移酶特异性说明
    教你使用java彻底掌握 “约瑟夫环”
    前后端发布分支规则
    如何学好C++?学习C和C++的技巧是什么?
    PCL 欧式聚类(EC)
    鹰潭高通量测序建设细节概述
  • 原文地址:https://blog.csdn.net/Medlar_CN/article/details/132845115