51单片机考核总结
前言
这个小电子秤算是我真正做的第一个小项目,虽然比较简陋,而且还有着一系列的问题存在,但是在探索的过程中还是学到了很多东西,收益颇丰。在此将一些硬件模块的用法以及原理总结,方便后续用到相同或者相似的模块时可较快的进行原理回顾与应用。
硬件部分
硬件部分也较为简单,除了51开发版上自带的模块,额外用到比较重要的模块有LM393比较器和薄膜传感器模块、HX711和压力传感器模块、I2C串转并模块(篇幅较大,后面再介绍)、以及LCD1602、矩阵键盘、无源蜂鸣器等较为传统的模块,在此不再赘述。
薄膜传感器和LM393比较器模块
薄膜传感器选用的是较为常见的FSR402电阻式压力传感器(如下图)
这款压力传感器是将施加在FSR传感器薄膜区域的压力转换成电阻值的变化,从而获得压力信息。压力越大,电阻越低。其允许用在压力0g-5kg的场合。根据 FSR402技术手册可以得知在输出管脚之间的的电阻和压力之间的关系如下。
在小压力阶段,当压力突破一定压力阈值之后,导通电阻有一个突破,这个阈值之前FSR相当于一个开关。当超过这个阈值的时候,FSR的电阻与压力之间就呈现一种连续变化的关系。
这种电阻的连续变换可对应压力大小,用来粗略估计施加压力(本题未实现)。
总得来说。该薄膜传感器主要是通过压力变化会导致阻值变化,从而导致后续LM393输出电平的变化。
LM393模块其本质上是一个电压比较器,芯片原理部分受限与所学知识(模电知识),未能完全掌握。后续了解之后再进行补充。因此主要介绍该模块的应用方法,模块图如下
右侧正负极分别接上面薄膜电阻模块的两端,左端四个接口,除去正负极以外两个口分别为DO(Digital ouput数字输出)和AO(Analogue output模拟输出),可以分别输出模拟信号和数字信号(1/0)。这里只用到了数字信号的输出,逻辑也较为简单:如果检测到电阻变化导致电压变化,DO口输出高电平,反之输出低电平。在单片机程序内检测对应IO口状态即可实现是否存在压力。代码如下:
#include <reg52.h> //52头文件
sbit lm393=P1^0;
sbit bing= P2^1; //位定义,蜂鸣器(LED不好整)
//主函数
void main() {
while(1){
if(lm393==1)bing=!bing;
}
}
HX711和压力传感器模块
模块总览如下图
[](https://imgse.com/i/piNJipd)
压力传感器与上面原理相似,也是随着压力变化电阻值进行改变,但在这里上下各有两个电阻精度更高,反应也更加灵敏
这里重点介绍HX711模块
硬件介绍
HX711压力传感器是一种高精度、高稳定性的模拟-数字转换器,它采用了24位Σ-Δ模数转换器和专用的前置放大器,能够将微小的压力变化转换为数字信号输出。具体来说,当外界施加压力时,传感器内部的应变片会发生微小的形变,这个形变会导致电桥电阻的变化,从而改变电桥的平衡状态,最终产生微小的电压信号。HX711传感器会将这个电压信号放大,并进行Σ-Δ调制,将模拟信号转换为数字信号输出,从而实现对压力变化的精确测量。
HX711芯片内部包含一个模拟前置放大器和一个24位的模数转换器。当HX711芯片的模拟输入引脚接入压力传感器时,传感器输出的微小电压信号首先被模拟前置放大器放大,然后再转换成数字信号输出。总的来说有两个功能,一个是信号放大,一个是模数转换,由于可直接输出数字信号,因此该传感器可直接接入单片机IO口进行数据传输,另外信号放大也可以使得微小的电压变化被捕捉到,提高精度。
左端输入端不用管,和压力传感器上四个引脚相连即可,关键是输出端,有四个引脚,除了VCC和GND以外,还有SCK和DOUT两个引脚,这和后面I2C串口通信相似,在此对他们两个进行简单介绍。
DOUT是HX711芯片的数据输出端口,它输出经过A/D转换处理后的24位数据。
SCK是HX711芯片的时钟输入端口,用于控制A/D转换的时钟。
因此可以根据时序图和手册进行数据接受,先接收到数字量数据再通过逆运算转换为真实重量,这里首先接受数据接收:
时钟脉冲数量对应增幅倍数,为了获得更高的精度,这里选取通道A 128倍增幅,即每个周期输出25个时钟脉冲
而时序图如下:
HX711为串行数据总线型A/D转化器。作为串行通讯方式,那么掌握其时序图对于该器件的使用和操作起到了至关重要的作用。串口通讯线由管脚PD-SCK和DOUT组成,用来输出数据,选择输入通道和增益。当数据输出管脚DOUT为高电平,表明A/D转换器还未准备好输出数据,此时串口时钟输入信号PD-SCK应为低电平。当DOUT从高电平变低电平后,PD-SCK应输入25至27个不等的时钟脉冲。其中第一个时钟脉冲的上升沿将读出输出24位数据的最高位(MSB),直至第24个时钟脉冲用来选择下一个A/D转换的输入通道和增益。
ad值接收
因此数据接收代码如下(ad值接收)
“HX711AD值接收函数”
#include "HX711.h" //在头文件中进行引脚的宏定义
//延时函数
void Delay__hx711_us(void)
{
_nop_();
_nop_();
}
//****************************************************
//读取HX711
//****************************************************
unsigned long HX711_Read(void) //增益128
{
unsigned long count; //用于接受每一次输出的ad值数据
unsigned char i;
HX711_DOUT=1; //先将DOUT置于高,表示未准备好
Delay__hx711_us();
HX711_SCK=0; //将时钟脉冲沿置低,
count=0;
EA = 1;
while(HX711_DOUT); //用一个空实现循环,当DOUT等于0时说明开始传输数据
EA = 0;
for(i=0;i<24;i++)
{
HX711_SCK=1; //时钟沿给1,说明允许DOUT传输一位数据
count=count<<1; //传输完这套数据后count左移,留出最右边的那一位给DOUT传输下一位
HX711_SCK=0; // 时钟沿取0,表示一个脉冲的结束,DOUT开始变换下一位数据
if(HX711_DOUT)
count++;} //这里用个简单的逻辑判断,如果DOUT为高,count最右位给1,否则给0。
//这样count可以和DOUt数据做到基本一致
HX711_SCK=1; //上面只给了24个时钟脉冲,这里多给一个,代表选择128倍增幅
count=count^0x800000;//第25个脉冲下降沿来时,
//转换数据,一说是将ad输出的补码改为原码
Delay__hx711_us(); //进行1us延时,为下一次传输作缓冲
HX711_SCK=0;
return(count);//将接收到的AD值输出
}
数据转换
输出的只是24位的二进制数,因此还要继续通过公式换算为真实的重量,换算如下:
因为实际和理论存在误差,为了提高精度,用理论大概得出一个范围,再根据测量出物体的实际大小,更改参数,测量重量相比实际重量偏大则将该参数调大,偏小调小即可。
注意,具体函数实现时,应该让什么都没有状态的初始AD值为毛皮重量(即使什么都不放AD值也不会为0),后续计算时候减去初始AD值即可得到真实重量。
软件部分
软件部分其实相对简单,写的代码也较为丑陋,这里就不把代码完整附上,主要对逻辑进行总结以及关键部分的回顾和一些问题的指出。
逻辑部分
- 首先,初始化了定时器、LCD、UART(串口通信)、和其他外部模块。
- 初始化电子秤的零点和毛皮重量,并将它们存储在变量 Weight_Init 和 Weight_Maopi 中。
- 进入主循环 while(1),并进行如下操作:
a. 调用 Get_Weight 函数来获取当前的物体重量,并将其存储在 Weight_Shiwu 变量中。
b. 通过矩阵按键扫描函数 MatrixKey 获按下的按键,并将结果存储在 Key 变量中。
c. 根据菜单选择不同的操作。菜单的显示和选择逻辑在 Menuuu、Menu1 和 Menu2 函数中实现。
d. 在主菜单中,你显示电子秤的重量、价格、以及是否为去皮模式(“QuPi” 或 “Real”)。
e. 根据选择的重量单位(weight_unit),将重量单位显示在LCD上(g、kg、pound或jing)。
f. 显示当前的物体重量(Weight_Shiwu)和价格(All_price),根据所选的重量单位进行适当的换算。
g. 通过串口通信将重量信息发送到外部设备。
h. 根据当前重量来调整PWM的频率(pwm_HZ)来控制LED的亮度,以反映当前的重量情况。
i. 如果用户按下 “Store” 按键(Key == 16),则将当前重量存储在数组 store 中。
总体来说,主程序具有以下功能:
- 显示菜单选项,并允许用户通过按键选择不同的菜单。
- 实时显示电子秤的重量,并可以切换不同的重量单位。
- 允许用户存储重量数据到数组 store 中。
- 通过串口通信将重量信息发送给外部设备。
- 根据电子秤的重量来控制LED的亮度。
菜单部分
其中菜单功能的实现代码如下
//不需要修改的可加code变为程序存储量数据,菜单显示界面需要的文字存储在一个数组中,节省单片机开支
char code Menu[5][16]={"1.QuPi_yes_no","2.Weight Unit","3.Price Unit","4.Data Storage", "1.QuPi_yes_no"};
//main函数中,用Key==4标志位进行跳转
if(Key==4){caidan=Menuuu();}//菜单选择界面,并且返回选择的功能(返回0就是无选择)
//caidan这一变量为另一标志位,当其不等于0时,跳转到相应功能界面
//选择菜单栏 (通过while循环让LCD保持在菜单选择,只有按下相应按键才进行跳转)
unsigned char Menuuu(){
LCD_WriteCommand(0x01);//清屏操作
i=0; //初始化i,方便后续操作
while(1){
Key=MatrixKey(); //读取相关按键返回值
if(Key==15){Delay(50);i++;LCD_WriteCommand(0x01);}//清屏操作;
if(Key==13){Delay(50);i--;LCD_WriteCommand(0x01);}//通过i++/i--控制LCD上显示的文字,达到滚动菜单的功能
if(i==4)i=0;
if(i<0)i=3; //简单的边界检测,防止i超出界限导致显示错误
LCD_ShowString(1,1,Menu[i]);
LCD_ShowString(2,1,Menu[i+1]);
LCD_ShowNum(1,16,0,1); //显示菜单文字主体
Delay(5);
if(Key==8){LCD_WriteCommand(0x01);return i+1;} //返回i+1可以让主函数跳转到另外一个相应的功能界面
if(Key==12){LCD_WriteCommand(0x01);return 0;} //如果按下返回键,返回0,即对主页面显示不做任何改变
}
}
重量读取和显示部分
//重量获取函数
void Get_Weight()
{
Weight_Shiwu = HX711_Read();
Weight_Shiwu = Weight_Shiwu - Weight_Maopi; //获取净重,减去毛皮重量
if(Weight_Shiwu > 0)
{
Weight_Shiwu = (unsigned int)((float)Weight_Shiwu/GapValue); //计算实物的实际重量克为单位
All_price=(unsigned int)((float)(Weight_Shiwu*Price*10/1000)); //顺便计算了价格
//这两个为不同单位下的重量转换
if(weight_unit==2)Weight_Shiwu=(unsigned int)((float)Weight_Shiwu/453*1000);//pound为单位(损失部分精度)
if(weight_unit==3)Weight_Shiwu=(unsigned int)((float)Weight_Shiwu/500*1000);//斤为单位(损失部分精度)
}
}
//毛皮重量获取(读取初始值)
void Get_Maopi()
{
Weight_Maopi = HX711_Read();
}
//下面是主函数中显示方式
if (Weight_Shiwu<0)Weight_Shiwu=0;
//让显示更加完美(不然在HX711不稳定情况下会出现AD值小于初始状态的情况)
//LED实时显示 g为单位 通过公式读取Weight_Shiwu的每一位数,而后加0x30则为对应的ASCII码(数字通用)
LCD_ShowChar(1,4,Weight_Shiwu/1000 + 0X30); //千
LCD_ShowChar(1,5,Weight_Shiwu%1000/100 + 0X30);//百
LCD_ShowChar(1,6,Weight_Shiwu%100/10 + 0X30); //十位
LCD_ShowChar(1,7,Weight_Shiwu%10 + 0X30); //个位
PWM部分(呼吸灯)
这次项目很重要的一点还让我学会了PWM波这一概念,通俗来讲就是由于单片机IO口输出的电压只有高低电平1/0,但如果想输出其他大小的电压进行相应控制则无能为力。因此这里引入PWM概念,通过控制占空比(即一个较短时间段内低电平占比,来控制该时间段的等效电压!达到输出不同电压的目的!)
其中几个较为关键的概念,PWM周期、频率、占空比解释和计算方法如下(理论上)
了解到了PWM波的概念和计算方法,本项目中采取频率为100HZ的PWM波(方便计算),即每秒可输出100个波形,PWM每个脉冲周期即为1000ms/100=10ms ,通过控制每个脉冲的高低即可控制PWM占空比。(如100个脉冲中10个高电平,其余为低电平,占空比为90%)
51单片机代码实现如下
//首先51单片机只能用定时器中断的形式来实现PWM波的输出
//每次计数计到100就执行命令并且清0(每一秒重新操作一次)
//定时器进入中断的频次是每一毫秒一次
//计数100次为一个PWM周期,每次耗时10ms,频率为100HZ,每个PWM周期为1s/100=10ms
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0xA4;
TH0 = 0xFF; //重新设置初值,确保每次定时时间相同
T0Count++;
//输出pwm脉冲100HZ PWM——HZ这个变量控制占空比(0-100),越高占空比越低(LED越暗)
//逻辑为如果Tcount小于设定的pwm_HZ,则令P2_0等于1,当Tcount达到pwm_HZ时候,令P2_0为0(低电平),所以当Tcount大于pwm_HZ的时候,P2_0保持低电平,反之则保持高电平。
if(T0Count==pwm_HZ)
{P2_0=0;
}
else if(T0Count==100){
P2_0=1;
T0Count=0;//重新开始下一个周期的pwm波
}
}
刚好最近做小车也用到PWM波,stm32可以直接由定时器输出PWM波,且最多指出4路输出更加方便,代码如下:
//初始化时候输入arr(自动装配值)和psc(预分频系数)
void TIM3_PWM_Init(u16 arr,u16 psc)
{ //GPIO口,TIM3和TIM3用于后续初始化
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
//GPIO口和TIM3时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//PWM对应输出GPIO口初始化(查手册,且要求复用推挽输出才可输出PWM波)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //设置PA6输出通道1
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//TIM3通道1 左轮!!
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //设置PA7输出通道2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//TIM3通道2 右轮!!
//PWM对应输出
///TIM3定时器初始化
TIM_TimeBaseStructure.TIM_Period = arr; //自动装配值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //预分频系数
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
//设置为pwm1模式,即小于给定值为1,大于给定值为0,即给定值PWM_left/right越大转速越快
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //设置为PWM输出模式1,即小于给定值为高电平,反之为低)
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC2Init(TIM3, &TIM_OCInitStructure); //PWM输出频道2初始化
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &TIM_OCInitStructure); //输出频道1初始化
TIM_Cmd(TIM3, ENABLE); //启用定时器计数功能
}
//TIM3PWM初始化函数(参数arr自动装配值,psc预分频系数)
TIM3_PWM_Init(1000,8000);//arr自动装配值给1000,psc预分频系数给8000,即每次计数到1000重新计数,晶振频率为8MHZ,即每秒计数
//8 000 000次
//计数频率为8 000 000/1000/8000=1次,即每秒重新装配一次
//输出PWM计数函数(标准库自带,1-pwm/arr即为占空比)
//参数pwm指的是计数时计到pwm即改变高低电平状态,pwm应该小于arr自动装配值
TIM_SetCompare1(TIM3,pwm);
重量单位选择部分
//重量单位选择菜单栏
unsigned char Menu2(){
i=0;//初始化循环
LCD_WriteCommand(0x01);//清清屏
while(1){
Key=MatrixKey();//获取按键值
//选择界面显示
LCD_ShowString(1,1,"g");
LCD_ShowString(1,10,"kg");
LCD_ShowString(2,1,"pound");
LCD_ShowString(2,10,"jing");
LCD_ShowString((i==0||i==1)?1:2,(i==0||i==2)?8:16,"O");
//最关键,可以通过三目运算符实现较为复杂的显示
if(Key==15){Delay(50);i++;LCD_WriteCommand(0x01);}//刷新操作并且更新i值
if(Key==13){Delay(50);i--;LCD_WriteCommand(0x01);}
//边界检测
if(i==4)i=0;
if(i<0)i=0;
caidan=0;//保证退出后直接返回菜单(标志位清0)
if(Key==8){LCD_WriteCommand(0x01);weight_unit=i;return i;}
}
}
上面最重要是学会了通过三目运算符较为便捷的实现逻辑较为复杂的判断,即i的四个取值分别对应4个不同的方向,可以通过三目运算符来枚举实现,且比if直接进行运算更快
问题总结
还有一些不足:
-
首先主函数一看就很冗余,没有进行适当封装,主要是考虑到定义了多个标志变量和外部变量,进行状态判定,担心定义过多全局变量过多影响单片机内存和效率(其实就是有点懒,主打一个能用就行,要是花点心思还是可以改善的)
-
没有依据单片机自身状态调整参数,参数是人为手调的(以某个特定物品为例,这样可能对其他物品的时候会有一定误差)
-
数据跳动没有处理,单纯delay减少跳动,当时是担心处理都会一定程度损失精度(硬件上加个托盘或者在电子设备少的地方,软件上可以设置一个时间区域的平均值为显示成果?或者每次进行检测,变动值小于一个值显示结果不变,)
-
变量名称定义不准确,一些变量只有自己可以明白(学长建议用结构体将同级别变量封装)
-
一些拓展功能如数字键盘改定价、速度测量没有实现,还有时候会出现归零失败等bug。
-
没有在最小系统板上完成
总的来说,后面还有很多地方值得改进,我也会吸取这次教训,继续前进!