一、题目
这是来自学单片机,上4T的模拟赛“第十五届蓝桥杯模拟考试III_嵌入式设计与开发”,编程题题目:
二、赛题用到的模块
首先,从题目图1可以知道用到了LED、按键、LCD、(这三个模块在专栏内已经有提到了)、ADC、串口,再加上必不可少的定时器,一共就是6个模块。
三、准备工作
先建一下基本的工程(在了解了模块后基本就可先做这些工作):
1、在STM32CubeMX中建立一个空白工程(【蓝桥杯嵌入式】创建空白工程-CSDN博客)
2、移植官方的LCD例程(【蓝桥杯嵌入式】LCD-CSDN博客)
3、配置LED,编写LED显示函数(【蓝桥杯嵌入式】点灯-CSDN博客)
4、编写状态机按键程序,这里使用独立检测版本(【蓝桥杯嵌入式】长短按 按键-CSDN博客里边的长按功能在key[]数组中关闭掉),按键能够简单控制LED即可。在这一步中就用到了定时器,定时器详情见【蓝桥杯嵌入式】状态机按键-CSDN博客
5、到了这里就差ADC和串口模块未加入了,接下来是阅读下题目内容,了解流程,理清大致的逻辑,可以在纸上进行个人总结(方便知道后续的编程)
四、屏幕显示
阅读题目中关于屏幕显示的规则(界面设置),一共有三个界面:产品参数、标准设置、合格率。
我们先不管界面的参数(那些可变的数字),先让我们的板子上能够显示出与题目一样的界面。
LCD的显示函数:lcd_disp()
LCD如何清屏、设置前后景颜色、显示字符的具体操作可以仿照例程。
我的实例:
// LCD界面显示函数
void lcd_disp()
{
// 产品参数界面
// "12345678901234567890"
LCD_DisplayStringLine(Line1, (unsigned char *)" GOODS "); // 第一行显示:商品信息标题
LCD_DisplayStringLine(Line3, (unsigned char *)" R37:1.25V "); // 第三行显示:R37电压值
LCD_DisplayStringLine(Line4, (unsigned char *)" R38:2.36V "); // 第四行显示:R38电压值
}
这样就可以得到符合题目要求的产品参数界面,接着再分别写出标准设置、合格率的界面(此时这些界面都是静态不可改变的,后续会添加参数实现实时显示)。
五、按键B1功能实现
按键B1是界面切换键,这样形成闭环的切换模式就需要我们对每个界面进行标记:
定义了一个界面标签变量lcd_mode
unsigned char lcd_mode = 0; // LCD显示界面的标签,0:产品参数界面;1:标准设置界面;2:合格率界面
那实现切换:0->1->2->0->1->······在单片机编程中常用的方法:取模
取模运算是一种常见的数学运算,用来求两个整数相除后的余数。在C语言中,取模运算使用百分号(%)来表示。
例如,如果我们有两个整数a和b,并且我们想求a除以b的余数,我们可以使用取模运算符%,如下所示:
int a = 10;
int b = 3;
int remainder = a % b; // 求10除以3的余数,结果为1取模运算的应用非常广泛,常用于以下几种情况:
- 确定一个数是偶数还是奇数:如果一个整数除以2的余数为0,那么它是偶数,否则它是奇数。
- 循环计数:取模运算可以用来循环计数。例如,当计数器达到一个指定的最大值后,通过取模运算将其重置为0,实现循环计数的功能。
- 分组操作:取模运算可以用于将一系列数据分成几个组,例如根据学生的年龄将他们分成几个年龄段。
// 按键B1的功能实现:界面翻转
void function_B1()
{
lcd_mode = (lcd_mode + 1) % 3; // 切换LCD界面模式
lcd_disp(); // 更新LCD显示
}
lcd_disp()中:
// LCD界面显示函数
void lcd_disp()
{
switch(lcd_mode)
{
case 0:
// 产品参数界面
// "12345678901234567890"
LCD_DisplayStringLine(Line1, (unsigned char *)" GOODS "); // 第一行显示:商品信息标题
LCD_DisplayStringLine(Line3, (unsigned char *)" R37:1.25V "); // 第三行显示:R37电压值
LCD_DisplayStringLine(Line4, (unsigned char *)" R38:2.36V "); // 第四行显示:R38电压值
break;
case 1:
// 标准设置界面
// "12345678901234567890"
······此处省略
break;
case 2:
// 合格率界面
// "12345678901234567890"
······此处省略
break;
default:
break;
}
}
六、按键B2、B3第一功能实现
按键B2、B3都是多功能按键,在不同界面下触发不同效果。它们的第一功能够是按下检测产品(检测R37,R38的电压值,这里用到ADC,ADC具体操作见----虚位以待----(ADC的博客还没开始写,写好了再补上doge))。在本题中,我的理解是ADC是只在B2、B3在产品参数界面时按下才会进行一次检测,但也有可能是ADC一直在检测(界面参数移植更新),B2、B3只是截取某时的结果进行存储、计算。
举例B2的电压采集,ADC相关函数后续再讲。
// 按键B2的功能实现
void function_B2()
{
if (lcd_mode == 0)
{
// 采集R37电压
unsigned int adc_dat; // ADC采集到的数据
char line_disp[21]; // 存储LCD行显示内容的数组,每行最多显示20个字符
// 启动ADC转换
HAL_ADC_Start(&hadc2);
// 等待ADC转换完成,超时时间为100毫秒
if (HAL_ADC_PollForConversion(&hadc2, 100) == HAL_OK)
{
// 读取ADC转换后的数据
adc_dat = HAL_ADC_GetValue(&hadc2);
// 计算R37电阻的电压值(0-3.3V)
adc_R37 = (adc_dat / 4095.0) * 330;
// 格式化R37电压值为字符串,保留两位小数
sprintf(line_disp, " R37:%.2fV ", (float)(adc_R37 / 100.0));
// 在LCD的第三行显示R37电压值
LCD_DisplayStringLine(Line3, (unsigned char *)line_disp);
// 停止ADC转换
HAL_ADC_Stop(&hadc2);
}
}
}
这里先讲怎么让显示界面上的内容参数化
一个很重要的函数:sprintf()(需要用到头文件stdio.h,记得在前边包含这个头文件进来)
sprintf()
函数是C语言中的一个标准库函数,用于将格式化的数据写入一个字符串中。它的原型如下:int sprintf(char *str, const char *format, ...);
其中,str
是一个指向字符数组的指针,用于存储格式化后的字符串;format
是一个格式化字符串,其中包含了要插入到最终字符串中的文本以及格式化占位符;...
表示可变数量的参数,这些参数将根据格式化字符串中的占位符进行格式化并插入到最终字符串中。格式化占位符是在格式化字符串中使用的特殊标记,用于指示
printf()
、sprintf()
、scanf()
等函数应该将什么样的数据插入到格式化后的字符串中,以及如何格式化这些数据。在C语言中,格式化占位符以%
符号开头,后跟一个或多个字符组成。以下是一些常见的格式化占位符及其用法:
%d
: 用于插入十进制整数。%f
: 用于插入浮点数,可以指定小数点后的位数。%s
: 用于插入字符串。%c
: 用于插入单个字符。%x
,%X
: 用于插入十六进制整数,小写或大写形式。%o
: 用于插入八进制整数。%u
: 用于插入无符号十进制整数。%p
: 用于插入指针地址。除了这些基本的格式化占位符外,还可以通过在占位符后添加修饰符来进一步控制格式化输出的方式。例如:
%10d
: 表示输出一个十进制整数,最小宽度为10,右对齐。%-10s
: 表示输出一个字符串,最小宽度为10,左对齐。%.2f
: 表示输出一个浮点数,保留两位小数。%04X
: 表示输出一个十六进制整数,最小宽度为4,并用0填充左侧空位。格式化占位符的使用使得
printf()
、sprintf()
等函数能够以一种灵活的方式格式化输出,并根据需要插入不同类型的数据。这使得在C语言中进行字符串操作变得更加方便和可控。
在示例里adc_R37保存的是R37上电压的100倍,例如电压测到是2.05V,而adc_R37记录的是205,使用整数的浮点型容易遇到精度问题(比如说是否等于==,递增++),所以慎用。但在用格式化函数sprintf()时,浮点型又很方便,所一这里又进行一次转换:
sprintf(line_disp, " R37:%.2fV ", (float)(adc_R37 / 100.0));
在大多数情况下,不推荐使用
==
来比较浮点数。虽然浮点数可以使用==
进行比较,但是由于浮点数的表示方式和运算规则,可能会导致精度问题,从而造成意外的结果。由于浮点数在计算机内部是以二进制形式表示的,因此在计算机中可能无法精确地表示某些十进制分数,例如0.1。这就导致了在浮点数计算中存在舍入误差,可能导致两个本应该相等的浮点数由于微小的差异而被误认为不等。
因此,当我们想要判断两个浮点数是否相等时,通常会采用以下方法之一
1.比较它们的差值是否小于某个很小的阈值:
float a = 0.1; float b = 0.2; float epsilon = 0.00001; // 设定一个很小的阈值 if (fabs(a - b) < epsilon) { // 浮点数a和b近似相等 }
2.比较它们的相对误差:
float a = 0.1; float b = 0.2; float maxRelativeError = 0.00001; // 设定一个很小的相对误差 if (fabs(a - b) / fmax(fabs(a), fabs(b)) < maxRelativeError) { // 浮点数a和b近似相等 }
同样的,与B2类似,设置下B3的第一功能(记得进行lcd_mode的判断,确保只在产品参数界面下触发采集功能)。
关于模拟赛的记录将会分为2~3次进行记录,更多内容敬请期待。