嵌入式计算器模块规划
上面我们的算法理论已经完善, 我们只用给一个混合运算式, 计算器就可以帮助我们计算出结果.
但是存在一个痛点, 每次计算算式,都要重新编译程序, 所以我们想到了, 利用单片机, 读取用户输入的按键, 组成算式, 输入给机器, 这样我们就可以利用上面的算法, 再次计算出结果了.
大致流程如下:
所以, 我们第一步做的,就是stm32利用矩阵按键识别键值
第二步,就是单片机把读取到的键值, 组合成算式
第三步,就是把算式传给算法,计算出结果
第四步, 就是把结果清零
拓展:第五步,可以把结果转换成语音播报
第一步: 矩阵按键,读取键值
(1)读取原理
首先我们要了解矩阵按键, 是干什么的, 顾名思义,矩阵按键,就是几行几列,依次排开的,按钮, 我们如果知道行,知道列,就可以锁定那个按键,所以我们只需要让单片机知道,我们按下的是哪一行,哪一列,那么单片机通过提前布局好的按键键值,就可以知道我们按下的是哪个按键了.
(2)按键布局
如下图, 即为我们矩阵按键的键盘布局:
第一行 ( ) / *
第二行 1 2 3 +
第三行 4 5 6 -
第四行 7 8 9 Esc
第五行 ← 0 → =
(3)按键原理图
矩阵按键原理图:
(4)单片机io口识别原理
我们锁定行列, 此矩阵按键有五行,四列, 我们就先识别按下哪一列吧,然后后面再锁定行,这样就可以得到坐标了.
单说, 按下按键, 怎么io口怎么识别出按下按键呢?
举一个例子, 单片机io口,相当于装满水的水杯, 单片机会检测io口水杯, 是满的,还是空的. 用户按下按键,就相当于, 把水倒掉,那么我们就可以让单片机检测到按键.
所以, 单片机检测io口,端口被称作输入口,顾名思义就是读取io口状态, 并把io口状态,反馈给单片机,那么水倒哪里了呢? 我们按下按键, 水就倒到了地下. 所以我们按键另一端按键接地是持续输出低电平的输出口.
<1>识别列
我们把上述的步骤, 复制四份, 我们就可以判断按下是哪一列了
此时,四个列检测io口, 链接四个按键,按键另一端连接的是地, 这个地可以用io口输出低电平代替,方便后续矩阵键盘拓展.
但是我们有四列, 五行, 所以, 每列都有,五个按钮, 我们紧接着, 再把他们排列起来.
<2>识别行
此时, 我们通过判断 PA1,PA2,PA3,PA4那个io口变化了,就可以识别到是哪列按键按下了,
但是我们是矩阵按键,还要进行行的识别.
其实,识别方法同理,我们只需要把, io口类型进行反转,把列控制端口全反转成输出低电平, 现在检测io口状态的变成每行的端口,当按下第一行按钮时候,对应的端口就会变成低电平, 单片机检测到,就可以识别到行 如下图所示
进行拓展后,就变成了矩阵按键,
<3> 识别原理总结
我们识别原理就是,先赋予列检测io口,高电平,设置其端口类型为输入口,读取按键状态,按键另一端,是输出口,持续输出低电平.
当我们按下按键的时候, 输入口io口的高电平会通过按键,送到低电平,此时单片机检测到低电平, 就判定是哪一列的按键按下了.
此时列已经锁定了, 下面开始锁定行,此时按下仍然处于按下状态.
我们此时设置控制行的io口,为输入口,检测按键状态, 控制列的io口,设置为输出低电平,那么此时按键是按下状态, 对应的行io口,电平就会从高电平->低电平, 那么单片机就会检测到io口状态变化, 我们就可以锁定对应的行.
行和列已经锁定,那么我们通过计算就可以得到我们按下的是矩阵按键的哪一个按键了.
<4>代码实现
我们使用线性反转法的核心思想,就是锁定行和列的坐标, 通过反转io口类型,检测io口状态,根据按键按下的状态,进而锁定坐标.
为了更快的进入列的选择,我们把四列io口,全部设置成中断形式,对应的行io口,全部设置为推挽输出低电平, 四列io口的中断触发方式,设置成下降沿触发.
①锁定列
这样我们就可以很快的进行, 锁定列了;
配置 PA1 ,PA2, PA3, PA4 为下降沿触发中断
1.中断初始化
c
void Exti_key_config(void)
{
//定义官方文档结构体
EXTI_InitTypeDef EXTI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//初始化
//使能时钟 PA AFIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
//io口初始化, 设置//使能内部上拉
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* Connect EXTI1 line to PA1 pin */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource1);
/* Connect EXTI2 line to PA2 pin */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource2);
/* Connect EXTI3 line to PA3 pin */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource3);
/* Connect EXTI4 line to PA4 pin */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource4);
//中断端口的设置(外部中断线 1,2,3和4, 端口模式, 什么触发: 下降沿 , 开启等)
/* Configure EXTI0 line */
EXTI_InitStructure.EXTI_Line = EXTI_Line1 | EXTI_Line2 | EXTI_Line3 | EXTI_Line4;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//下面配置中断优先级
/* Configure EXTI1 interrupt */
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* Configure EXTI2 interrupt */
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;
NVIC_Init(&NVIC_InitStructure);
/* Configure EXTI3 interrupt */
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;
NVIC_Init(&NVIC_InitStructure);
/* Configure EXTI4 interrupt */
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;
NVIC_Init(&NVIC_InitStructure);
}
2.触发中断
char key_number; // 1--4 (代表一到四列)
_Bool key_button_down; //代表按键按下
c
void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) != RESET)
{
delay(0x20000); //中断服务函数当中 , 是不能用大延时的,快进 快出
/* Your code goes here */
key_number = 1;
key_button_down = 1;
//中断服务函数当中 , 是不能用大延时的,快进 快出
delay(0x20000);
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
void EXTI2_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line2) != RESET)
{
//中断服务函数当中 是不能用大延时的,快进 快出
delay(0x20000);
/* Your code goes here */
key_number = 2;
key_button_down = 1;
//中断服务函数当中 , 是不能用大延时的,快进快出
delay(0x20000);
EXTI_ClearITPendingBit(EXTI_Line2);
}
}
void EXTI3_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line3) != RESET)
{
//中断服务函数当中 , 是不能用大延时的,快进快出
delay(0x20000);
/* Your code goes here */
key_number = 3;
key_button_down = 1;
//中断服务函数当中 , 是不能用大延时的,快进快出
delay(0x20000);
EXTI_ClearITPendingBit(EXTI_Line3);
}
}
void EXTI4_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line4) != RESET)
{
//中断服务函数当中 , 是不能用大延时的,快进快出
delay(0x20000);
/* Your code goes here */
key_number = 4;
key_button_down = 1;
//中断服务函数当中 , 是不能用大延时的,快进快出
delay(0x20000);
EXTI_ClearITPendingBit(EXTI_Line4);
}
}
3.锁定列数值
c
extern char key_number;
extern char chose_column;//选中的列
void find_column(void)
{
if(key_number == 1)
{
chose_column = 1;
}
else
if(key_number == 2)
{
chose_column = 2;
}
else
if(key_number == 3)
{
chose_column = 3;
}
else
if(key_number == 4)
{
chose_column = 4;
}
else
{
}
}
②锁定行
此时列已经锁定, 我们把列io口,全变成输出低电平, 控制行的io口,变成输入口,高电平, 此时单片机检测io的变化,就可以检测到哪行按下了.
1.端口类型转换
c
/* 切换为推挽输出模式 GPIO_Mode_Out_PP*/
//下面进行判断是哪个行(对调模式)
//GPIOA, GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4
void exchange_Mode(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//初始化
//使能时钟 PA AFIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
//io口初始化, 设置//使能内部上拉
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 切换为下拉输入输入模式 GPIO_Mode_IPD */
//GPIOA, GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7
//GPIOB, GPIO_Pin_0 | GPIO_Pin_10
//io口初始化, 设置//使能内部上拉
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
2.锁定行
电平反转, 类型反转,当检测到行io口电平变成低电平,就可以锁定行了
c
extern char chose_line;//选中的行
void find_line(void) //行
{
GPIO_SetBits(GPIOA, GPIO_Pin_1);
GPIO_SetBits(GPIOA, GPIO_Pin_2);
GPIO_SetBits(GPIOA, GPIO_Pin_3);
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
GPIO_ResetBits(GPIOA, GPIO_Pin_6);
GPIO_ResetBits(GPIOA, GPIO_Pin_7);
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
GPIO_ResetBits(GPIOB, GPIO_Pin_10);
delay(0x20000);
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5) == SET)
{
delay(0x20000);
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5) == SET);
chose_line = 1;
}
else
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == SET)
{
delay(0x20000);
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == SET);
chose_line = 2;
}
else
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == SET)
{
delay(0x20000);
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == SET);
chose_line = 3;
}
else
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET)
{
delay(0x20000);
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET);
chose_line = 4;
}
else
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == SET)
{
delay(0x20000);
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == SET);
chose_line = 5;
}
}
③ 计算键值
//根据所在行列 ,计算出特定符号
c
char compute_sign(char lines,char columns)
{
//算出特定需要
char counter;
char sign;
counter = (lines-1)*4 + columns;
/*
行 1 2 3 4 (列)
1 ( ) / *
2 1 2 3 +
3 4 5 6 -
4 7 8 9 Esc
5 <- 0 -> =
*/
switch(counter)
{
case 1:
sign = '(';
break;
case 2:
sign = ')';
break;
case 3:
sign = '/';
break;
case 4:
sign = '*';
break;
case 5:
sign = '1';
break;
case 6:
sign = '2';
break;
case 7:
sign = '3';
break;
case 8:
sign = '+';
break;
case 9:
sign = '4';
break;
case 10:
sign = '5';
break;
case 11:
sign = '6';
break;
case 12:
sign = '-';
break;
case 13:
sign = '7';
break;
case 14:
sign = '8';
break;
case 15:
sign = '9';
break;
case 16:
sign = 'x';
break;
case 17:
sign = '<';
break;
case 18:
sign = '0';
break;
case 19:
sign = '>';
break;
case 20:
sign = '=';
break;
default:
sign = 0;
break;
}
return sign;
}
④ main函数调用展示键值
<1>软件初始化
c
void Software_Init(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断控制器分组设置
OLED_Init();
Delay_Init();
// uart_init();//波特率115200
uart1_init(115200);
//线性反转第一波(等待按键按下)
button_key_config();
Exti_key_config();
init_collect_data();//按键收集中缀式初始化
}
<2>按键展示在OLED上
c
//按键展示
void show_keyboard(void)
{
int counter;
//展示选中的数字
OLED_ShowChar(80, 0, show_sign, OLED_8X16);
if(collect_space.counter == collect_space.insert_locate && (show_sign != '<') && (show_sign != '>'))
{
OLED_ClearArea(0, 40, 128, 16);
//for循环输出字符
for(counter = 0; counter < collect_space.counter ;counter++)
{
OLED_ShowChar(5+(counter)*14, 40, collect_space.data_symbol[counter], OLED_8X16);
}
}
else
if(collect_space.counter != collect_space.insert_locate)
{
OLED_ClearArea(0, 40, 128, 16);
//for循环输出字符
for(counter = 0; counter < collect_space.counter ;counter++)
{
OLED_ShowChar(5+(counter)*14, 40, collect_space.data_symbol[counter], OLED_8X16);
}
}
OLED_Update();
}
<3>循环检测按键按下
c
while(1)
{
//按键按下
if(key_button_down == 1)
{
//识别按键
scan_keyboard(); //注意,此时按键应该抬起
//拾取按键到中缀式
collect_Key_information(show_sign);
//按键展示
show_keyboard();
//擦屁股, 恢复读取模式 线性反转第一波(等待按键按下)
button_key_config();
Exti_key_config();
key_button_down = 0;
}
}
第二步: 单片机键值, 组合成算式字符串
(1)收集框架
我们收集的算式, 包括,加减乘除,'0'-'9',还有左右括号,至于等于号,就不需要了,因为按完算式后,按下等于号, 相当于计算结果,等计算出结果后, 我们再按下等于,相当于清零,所以我们要做一下区分.
(2)中缀式数据结构
所以, 我们定义一个中缀算式的数据结构
c
#define data_MaxSize 100
//中缀式处理结构体
struct CollectSpace
{
char data_symbol[data_MaxSize]; //存储中缀式符号的数组
int counter; //最后一个字符的数组位置
int insert_locate; //当前需要插入的位置
_Bool Start_Mode; //是否开始计算
_Bool clear; //是否清零
};
(3)读取开始运算标志,计算结果
如果第一次按下等于号 ,就开始计算
c
if(collect_space.Start_Mode == 1) //判断中缀式处理结构,是否进入计算模式
{
//中缀式结构数组 --> 后缀式
Conversion_expression(collect_space.data_symbol,Suffix_expression);//转换后缀式
//计算数值
Calculate_result = Calculate_value(Suffix_expression);
OLED_Clear();
//结果
OLED_Printf(0,0, OLED_8X16,"%.2f", Calculate_result);
//总算式
OLED_ShowString(0,20, collect_space.data_symbol,OLED_8X16);
OLED_Update();
collect_space.Start_Mode = 0; //停止计算
}
(4)收集按键信息
①分按键类型,进入不同模式
收集的信息, 分为数字,运算符和等于号,
我们分别根据类型, 进入不同的模式进行处理:
判断对应类型,进入不同处理模式
c
void collect_Key_information(char collect_key)
{
//获得按键
//查询功能
if(collect_key >= '0' && collect_key <= '9')
{
char_input.character_Mode = 1; //数字模式
}
else
if(
collect_key == '+' ||
collect_key == '-' ||
collect_key == '*' ||
collect_key == '/' ||
collect_key == '(' ||
collect_key == ')' )
{
char_input.character_Mode = 2; //运算符模式
}
else
if(collect_key == '<'||collect_key == '>')
{
char_input.character_Mode = 3; //移动编辑模式
}
else
if(collect_key == 'x')
{
char_input.character_Mode = 4; //编辑删除模式
}
else
if(collect_key == '=')
{
char_input.character_Mode = 5; //运算结果模式
}
deal_mode(collect_key);
}
② 根据不同模式,进入不同的处理
<1>数字模式
数字可以插入第一个位置和 第字符个数+1的位置
处理方法: 先腾出要插入的位置,然后把对应位置插入
c
if(char_input.character_Mode == 1)
{
//合法性判断(可以插入第一个位置, 和 第(字符个数 + 1)的位置)直接使用物理序号
if(collect_space.insert_locate >= 0 && collect_space.insert_locate <= collect_space.counter)
{
for(j = collect_space.counter; j > collect_space.insert_locate; j--)
{
collect_space.data_symbol[j] = collect_space.data_symbol[j-1];
}
collect_space.data_symbol[collect_space.insert_locate] = deal_key;
//合法后 , 字符数量加1
collect_space.counter++;
//默认下次光标插入位置++
collect_space.insert_locate++;
}
}
<2>运算符模式
(可以插入第一个位置, 和 第(字符个数 + 1)的位置)直接使用物理序号
c
if(char_input.character_Mode == 2) //加入合法性判断
{
//合法性判断(可以插入第一个位置, 和 第(字符个数 + 1)的位置)直接使用物理序号
if(collect_space.insert_locate >= 0 && collect_space.insert_locate <= collect_space.counter)
{
for(j = collect_space.counter; j > collect_space.insert_locate; j--)
{
collect_space.data_symbol[j] = collect_space.data_symbol[j-1];
}
collect_space.data_symbol[collect_space.insert_locate] = deal_key;
//合法后 , 字符数量加1
collect_space.counter++;
//默认下次光标插入位置++
collect_space.insert_locate++;
}
}
<3>移动编辑模式
这里, 我们只是切换了光标序号,所以我们下次插入字符的时候,就不能直接覆盖了,而是腾出位置,然后插入了
c
if(char_input.character_Mode == 3) //移动编辑插入字符模式
{
//光标跟踪
if(deal_key == '<')
{
if(collect_space.insert_locate > 0)
{
collect_space.insert_locate--;
}
}
else if(deal_key == '>')
{
if(collect_space.insert_locate < collect_space.counter)
{
collect_space.insert_locate++;
}
}
}
<4>删除字符模式
我们删除字符前, 要做删除位置合法性判断(删除位置 = 插入位置的前一个字符)
//合法性判断(可以删除的位置 0 ~ 数组的collect_space.counter-1)
c
if(char_input.character_Mode == 4) //删除字符模式
{
//删除位置合法性判断(删除位置 = 插入位置的前一个字符)
deletesapce.now_delete = collect_space.insert_locate-1;
//合法性判断(可以删除的位置 0 ~ 数组的collect_space.counter-1)
if(deletesapce.now_delete >= 0 && deletesapce.now_delete < collect_space.counter)
{
//删除光标处的前一个字符 === //把光标后的字符移动到此为止(覆盖)
//临界 data[collect_space.counter-2 ] = data[collect_space.counter-1 ];
//==> j = collect_space.counter-2 =得出范围=> j < collect_space.counter-1
for(j = deletesapce.now_delete; j < collect_space.counter-1; j++)
{
collect_space.data_symbol[j] = collect_space.data_symbol[j+1];
}
//数组个数减去1
collect_space.counter--;
//光标位序减去1
collect_space.insert_locate--;
}
}
<5>运算模式
当我们第一次按下按键,我们就进入运算模式, 并且加一个自锁变量,下次等于就是清零变量,collect_space.clear ^= 1;
c
if(char_input.character_Mode == 5) //运算模式
{
//开启计算结果,并将计算数值装入中缀式
if(collect_space.clear == 0)
{
collect_space.data_symbol[collect_space.counter] = '\0';
collect_space.Start_Mode = 1;
//中缀式结构 进入下次计算的待计算模式(擦屁股)//分情况
collect_space.counter = 0;
collect_space.insert_locate = 0;//初始的时候, 是数组, 0
collect_space.clear ^= 1;
}
else
if(collect_space.clear == 1) //将计算清零(归零):擦屁股
{
//清空屏幕
OLED_Clear();
OLED_Update();
collect_space.clear ^= 1;
}
}
第三步,计算结果
我们通过观看博客原理,即可
https://blog.csdn.net/qq_57484399/article/details/138288148
第四步,屏幕显示
我们可以直接调用OLED显示函数,显示结果
但是如果我们需要使用语音,读出结果的话,就要对每一位进行分个进行读取,然后结合个十百千万以及汉语的语言习惯了,我会单独出一个博客,讲解,如果把自然数小数进行语音播报.
第五步, 清零
再次按下等于号,即可清零