1. 项目介绍

称重计量是现在社会活动中不可缺少的部分,随着国际交流的发展,称重计量的国际间的统一显得越来越重要。

电子称重技术是现代称重计量和控制系统工程的重要基础之一。近年来,随着现代科技进步,电子称重技术取得了突飞猛进的发展,电子秤在计量领域中也占有越来越重要的地位。尤其是商用电子衡器,以其准确度高、反应灵敏、性能稳定、结构简单、环境适应性强、便于与电子计算机结合而实现称重计量与过程控制自动化等特点,而被广泛用于工商贸易、能源交通、冶金矿山、轻工食品、医药卫生、航空航天等领域。

电子秤的工作原理首先是通过称重传感器采集到被测物体的重量并将其转换成电压信号,输出电压信号通常很小,需要通过前端信号处理电路进行准确的线性放大。放大后的模拟电压信号经A/D转换电路转换成数字量,被送入到主控电路的单片机中,再经过单片机控制OLED显示屏,从而显示出被测物体的重量,在实际应用中为提高数据采集的精度,并尽量减少外界电气干扰还需要在传感器与A/D芯片之间加上信号调理电路。

当前项目是采用采用STM32+称重模块+OLED实现了简单的电子秤项目,称重模块上采用24位的ADC芯片,精度较高。实现了称重,校准、去皮等功能。

硬件介绍:

MCU:STM32F103ZET6,只要是STM32F1X系列本工程代码都通用的。

称重模块: 淘宝购买的称重模块

OLED: SPI接口的0.96寸OLED屏,采用的是中景园电子的OLED屏。

完整工程下载地址: https://download.csdn.net/download/xiaolong1126626497/63993934

视频演示地址: https://live.csdn.net/v/182608

项目运行效果:

image-20211221160445064

image-20211221160459187

image-20211221160520611

2. 项目实现

2.1 称重传感器

称重传感器就是一个压力传感器,其又叫做悬臂梁压力传感器。安装时需要一端固定,另一端受力。其内部有四个应变电阻片,共同组成了一个电桥,当受力端施加压力时,传感器壳体会发生形变,从而影响应变电阻片的阻值。

下面是称重传感器的原理图:

image-20211221160603395

称重传感器实物图:

image-20211221160642622

称重传感器是采用 CS1237 作为转换芯片,用于把微小的电压信号转换为具有 24 位精度的数字信号。模块信号输入端可以接受差分信号,内部具有可编程运算放大器用于放大输入端的弱小信号。模块内置温度传感器,可粗略估计周围温度。模块可用于多种工业过程控制场合,比如电子秤,血液计,智能变换器等。

CS1237中有1路ADC,集成了1路差分输入,信号输入可以是差分输入信号AINP、AINN,也可以是温度传感器的输出信号,输入信号的切换由寄存器(ch_sel[1:0])控制。

CS1237是采用2线SPI串行通信,通过SCLK和DRDY/DOUT可以实现数据的接收以及功能配置。

实现代码如下:

#include "ADC-CS1237.h"


static long AD_Res_Last=0;																		//上一轮的ADC数值保存
/*
定义CS1237使用的GPIO口
CLK   PB14    时钟线
OUT   PB15    数据输出线
*/
void CS1237_GPIO_INIT(void)
{
	RCC->APB2ENR |= 0x01 << 3;    											//打开PB口
	
	GPIOB->CRH &= 0xF0FFFFFF;														//寄存器清零
	GPIOB->CRH |= 0x03000000;														//通用推挽输出 50MHz
	GPIOB->ODR |= 1<< 14;																//拉高CLK电平
}

void CS1237_DRDY(void)																//配置PB15为输入
{
	GPIOB->CRH &= 0x0FFFFFFF;														//寄存器清零
	GPIOB->CRH |= 0x80000000;														//上下拉输入模式
}


void CS1237_DOUT(void)																//配置PB15为输出
{
	GPIOB->CRH &= 0x0FFFFFFF;														//寄存器清零
	GPIOB->CRH |= 0x30000000;														//通用推挽输出 50MHz
}



//CS1237进入低功耗模式
void CS1237_Power_Down(void)
{
	CLK_HIGH
	delay_us(200);                											//CLK上拉时间应超过100us,恢复后下拉时间至少10us
}
//配置CS1237芯片
void Con_CS1237(void)
{
	u8 i;
	u8 dat;
	u8 count_i=0;																				//溢出计时器
	dat = CS_CON;																				// 0100 1000
	CS1237_DOUT();
	OUT_HIGH
	delay_ms(310);																			//上电建立时间
	CS1237_DRDY();																			//配置PB15为输入	
	CLK_LOW																							//时钟拉低
	while(INT)																					//芯片准备好数据输出  时钟已经为0,数据也需要等CS1237全部拉低为0才算都准备好
	{
		printf("123\r\n");
		delay_ms(100);																		//10HZ下转换时间是100ms
		count_i++;
		if(count_i > 150)
		{
			CLK_HIGH;
			CS1237_DOUT();
			OUT_HIGH
			return;																					//超时,则直接退出程序
		}
	}
	for(i=0;i<29;i++)																	  // 1 - 29
	{
		One_CLK;
	}
	CS1237_DOUT();
	CLK_HIGH;delay_us(6);OUT_HIGH;CLK_LOW;delay_us(6);	//30
	CLK_HIGH;delay_us(6);OUT_HIGH;CLK_LOW;delay_us(6);	//31
	CLK_HIGH;delay_us(6);OUT_LOW;CLK_LOW;delay_us(6);		//32
	CLK_HIGH;delay_us(6);OUT_LOW;CLK_LOW;delay_us(6);		//33
	CLK_HIGH;delay_us(6);OUT_HIGH;CLK_LOW;delay_us(6);	//34
	CLK_HIGH;delay_us(6);OUT_LOW;CLK_LOW;delay_us(6);		//35
	CLK_HIGH;delay_us(6);OUT_HIGH;CLK_LOW;delay_us(6);	//36
	One_CLK;																						//37     写入了0x65
	for(i=0;i<8;i++)																		// 38 - 45个脉冲了,写8位数据
	{
		CLK_HIGH;
		delay_us(6);
		if(dat&0x80)
			OUT_HIGH
		else
			OUT_LOW
		dat <<= 1;
		CLK_LOW;
		delay_us(6);
	}
	CS1237_DRDY();
	One_CLK;												//46个脉冲拉高数据引脚
}

//读取芯片的配置数据
u8 Read_CON(void)
{
	u8 i;
	u8 dat=0;												//读取到的数据
	u8 count_i=0;										//溢出计时器
	CS1237_DOUT();
	OUT_HIGH
	CS1237_DRDY();									//配置PB15为输入	
	CLK_LOW													//时钟拉低
	while(INT)											//芯片准备好数据输出  时钟已经为0,数据也需要等CS1237全部拉低为0才算都准备好
	{
		delay_ms(100);
		count_i++;
		if(count_i > 150)
		{
			CLK_HIGH;
			CS1237_DOUT();
			OUT_HIGH;
			return 1;										//超时,则直接退出程序
		}
	}
	for(i=0;i<29;i++)								// 1 - 29
	{
		One_CLK;
	}
	CS1237_DOUT();
	CLK_HIGH;delay_us(6);OUT_HIGH;CLK_LOW;delay_us(6);//30
	CLK_HIGH;delay_us(6);OUT_LOW;CLK_LOW;delay_us(6);//31
	CLK_HIGH;delay_us(6);OUT_HIGH;CLK_LOW;delay_us(6);//32
	CLK_HIGH;delay_us(6);OUT_LOW;CLK_LOW;delay_us(6);//33
	CLK_HIGH;delay_us(6);OUT_HIGH;CLK_LOW;delay_us(6);//34
	CLK_HIGH;delay_us(6);OUT_HIGH;CLK_LOW;delay_us(6);//35
	CLK_HIGH;delay_us(6);OUT_LOW;CLK_LOW;delay_us(6);//36
	One_CLK;//37     写入了0x56
	CS1237_DRDY();
	dat=0;
	for(i=0;i<8;i++)											// 38 - 45个脉冲了,读取数据
	{
		One_CLK;
		dat <<= 1;
		if(INT)
			dat++;
	}
	One_CLK;															//46个脉冲拉高数据引脚
	return dat;
}

//读取ADC数据,返回的是一个有符号数据
long Read_CS1237(void)
{
	u8 i;
	long dat=0;														//读取到的数据
	u16 count_i=0;												//溢出计时器
	CS1237_DOUT();
	OUT_HIGH															//等待模拟输入信号建立
	CLK_LOW;															//时钟拉低
	CS1237_DRDY();
	while(INT)														//芯片准备好数据输出  时钟已经为0,数据也需要等CS1237拉低为0才算都准备好
	{
//		printf("等待1\r\n");
		delay_ms(10);
		count_i++;
		if(count_i > 300)
		{
			CLK_HIGH;
			CS1237_DOUT();
			OUT_HIGH;
			return 0;													//超时,则直接退出程序
		}
	}
	CS1237_DOUT();
	OUT_HIGH															//端口锁存1,
	CS1237_DRDY();
	dat=0;
	for(i=0;i<24;i++)											//获取24位有效转换
	{
		CLK_HIGH;
		delay_us(6);
		dat <<= 1;
		if(INT)
			dat ++;
		CLK_LOW;
		delay_us(6);	
	}
	for(i=0;i<3;i++)											//一共输入27个脉冲
	{
		One_CLK;
	}
	CS1237_DOUT();
	OUT_HIGH;
	//先根据宏定义里面的有效位,丢弃一些数据
	i = 24 - ADC_Bit;//i表示将要丢弃的位数
	dat >>= i;//丢弃多余的位数
	return dat;
}

//初始化ADC相关参数
int Init_CS1237(void)
{
	Con_CS1237();//配置CS1237
	if(Read_CON() != CS_CON)//如果读取的ADC配置出错,则重启
	{
		printf("读取错误\r\n");
		return 0;
	}
	delay_us(10000);
	AD_Res_Last = Read_CS1237();
	AD_Res_Last = Read_CS1237();
	AD_Res_Last = Read_CS1237();
	return 0;
}

//数字一阶滤波器 滤波系数A,小于1。上一次数值B,本次数值C out = b*A + C*(1-A)
//下面这个程序负责读取出最终ADC数据
long Read_18Bit_AD(void)   //18位的
{
	float out,c;
	
	out = AD_Res_Last;
	c = Read_CS1237();
	if(c!=0) // 读到正确数据
	{
		out = out*Lv_Bo + c*(1-Lv_Bo);
		AD_Res_Last = out;//把这次的计算结果放到全局变量里面保护
	}
	return AD_Res_Last;
}

2.2 OLED显示屏

OLED显示屏是0.96寸 SPI接口显示屏,采用SSD1306驱动,兼容3.3V或5V电源输入,非常常见,淘宝一搜一大堆,当前选择的是中景园电子的OLED显示屏。

在调试设备或者测试数据时,有时候需要实时观察数据的变化,加入显示屏可以把观察设备的运行情况,数据变化等。在成本和难易程度上,OLED显示屏是非常适合初学者去学习与应用的。

OLED视频实物图:

image-20211221161443871

示例代码:

#include "OLED.H"
#include "oled_font.h"

/*
定义OLED使用的GPIO口
D0   PA5    时钟线	
D1   PA1    数据输出线
RES  PA2    复位线
DC   PA3    数据/命令选择线
CS   PA4    片选线
*/

void OLED_GPIO_INIT(void)
{
	RCC->APB2ENR |= 1<<2;       //打开PA口
	GPIOA->CRL &= 0xFF00000F;	//寄存器清零
	GPIOA->CRL |= 0x00333330;  //通用推挽输出 50MHz
	GPIOA->ODR |=0x003E;
}

void OLED_Init(void)
{
	OLED_GPIO_INIT();	//GPIO口初始化
 
	OLED_RES_HIGH;
	delay_ms(100);
	OLED_RES_LOW;
	delay_ms(200);								//延迟,由于单片机上电初始化比OLED快,所以必须加上延迟,等待OLED上电初始化完成
	OLED_RES_HIGH;
	delay_ms(200);

	OLED_WR_Byte(0xAE,OLED_CMD);	//关闭显示
	OLED_WR_Byte(0x2e,OLED_CMD);	//关闭滚动

	OLED_WR_Byte(0x00,OLED_CMD);	//设置低列地址
	OLED_WR_Byte(0x10,OLED_CMD);	//设置高列地址
	OLED_WR_Byte(0x40,OLED_CMD);	//设置起始行地址
	OLED_WR_Byte(0xB0,OLED_CMD);	//设置页地址

	OLED_WR_Byte(0x81,OLED_CMD);	// 对比度设置,可设置亮度
	OLED_WR_Byte(0xFF,OLED_CMD);	//  265  

	OLED_WR_Byte(0xA1,OLED_CMD);	//设置段(SEG)的起始映射地址;column的127地址是SEG0的地址
	OLED_WR_Byte(0xA6,OLED_CMD);	//正常显示;0xa7逆显示

	OLED_WR_Byte(0xA8,OLED_CMD);	//设置驱动路数
	OLED_WR_Byte(0x3F,OLED_CMD);	//1/64duty
	
	OLED_WR_Byte(0xC8,OLED_CMD);	//重映射模式,COM[N-1]~COM0扫描

	OLED_WR_Byte(0xD3,OLED_CMD);	//设置显示偏移
	OLED_WR_Byte(0x00,OLED_CMD);	//无偏移
	
	OLED_WR_Byte(0xD5,OLED_CMD);	//设置震荡器分频(默认)
	OLED_WR_Byte(0x80,OLED_CMD);	
	
	OLED_WR_Byte(0xD8,OLED_CMD);	//设置 area color mode off(没有)
	OLED_WR_Byte(0x05,OLED_CMD);
	
	OLED_WR_Byte(0xD6,OLED_CMD);	//放大显示
	OLED_WR_Byte(0x00,OLED_CMD);
	
	OLED_WR_Byte(0xD9,OLED_CMD);	//设置 Pre-Charge Period(默认)
	OLED_WR_Byte(0xF1,OLED_CMD);
	
	OLED_WR_Byte(0xDA,OLED_CMD);	//设置 com pin configuartion(默认)
	OLED_WR_Byte(0x12,OLED_CMD);
	
	OLED_WR_Byte(0xDB,OLED_CMD);	//设置 Vcomh,可调节亮度(默认)
	OLED_WR_Byte(0x30,OLED_CMD);
	
	OLED_WR_Byte(0x8D,OLED_CMD);	//设置OLED电荷泵
	OLED_WR_Byte(0x14,OLED_CMD);	//开显示
	
	OLED_WR_Byte(0xA4,OLED_CMD);	// Disable Entire Display On (0xa4/0xa5)
	OLED_WR_Byte(0xA6,OLED_CMD);	// Disable Inverse Display On (0xa6/a7) 
	
	OLED_WR_Byte(0xAF,OLED_CMD);	//开启OLED面板显示
	OLED_Clear();	//清屏
	OLED_Set_Pos(0,0); 	 //画点
}

void OLED_Write_Byte(u8 data)
{
	u8 i;	//定义变量
	for(i = 0; i < 8; i++)	//循环8次
	{
		OLED_D0_LOW		//将时钟线拉低
		delay_us(1);	//延迟		
		if(data & 0x80)	//数据从高位-->低位依次发送
			OLED_D1_HIGH	//数据为为1
		else
			OLED_D1_LOW	//数据位为0		
		data <<= 1;	//数据左移1位
		OLED_D0_HIGH	//时钟线拉高,把数据发送出去
		delay_us(1);	//延迟
	}
	
}


/*
	@brief			对OLED写入一个字节
	@param			dat:数据
					cmd:1,写诶数据;0,写入命令
	@retval			无
 */
void OLED_WR_Byte(u8 dat,u8 cmd)
{
	if(cmd)  //如果cmd为高,则发送的是数据
		OLED_DC_HIGH	//将DC拉高
	else    //如果cmd为低,则发送的是命令
		OLED_DC_LOW	//将DC拉低
		
	OLED_CS_LOW; //片选拉低,选通器件
		
	OLED_Write_Byte(dat); //发送数据
		
	OLED_CS_HIGH //片选拉高,关闭器件 
	OLED_DC_HIGH //DC拉高,空闲时为高电平
}


/*
	@brief			设置数据写入的起始行、列
	@param			x: 列的起始低地址与起始高地址;0x00~0x0f:设置起始列低地址(在页寻址模式);
						0x10~0x1f:设置起始列高地址(在页寻址模式)
					y:起始页地址 0~7
	@retval			无
 */
void OLED_Set_Pos(u8 x, u8 y) 
{ 
	OLED_WR_Byte(0xb0+y,OLED_CMD);//写入页地址
	OLED_WR_Byte((x&0x0f),OLED_CMD);  //写入列的地址  低半字节
	OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);//写入列的地址 高半字节
}   	     	  


/*
	@brief			开显示
	@param			无
	@retval			无
 */ 
void OLED_Display_On(void)
{
	OLED_WR_Byte(0X8D,OLED_CMD);  //设置OLED电荷泵
	OLED_WR_Byte(0X14,OLED_CMD);  //使能,开
	OLED_WR_Byte(0XAF,OLED_CMD);  //开显示
}

/*
	@brief			OLED滚屏函数,范围0~1页,水平向左
	@param			无
	@retval			无
 */	
void OLED_Scroll(void)
{
	OLED_WR_Byte(0x2E,OLED_CMD);	//关闭滚动
	OLED_WR_Byte(0x27,OLED_CMD);	//水平向左滚动
	OLED_WR_Byte(0x00,OLED_CMD);	//虚拟字节
	OLED_WR_Byte(0x00,OLED_CMD);	//起始页 0
	OLED_WR_Byte(0x00,OLED_CMD);	//滚动时间间隔
	OLED_WR_Byte(0x01,OLED_CMD);	//终止页 1
	OLED_WR_Byte(0x00,OLED_CMD);	//虚拟字节
	OLED_WR_Byte(0xFF,OLED_CMD);	//虚拟字节
	OLED_WR_Byte(0x2F,OLED_CMD);	//开启滚动
}

/*
	@brief			关显示
	@param			无
	@retval			无
 */  
void OLED_Display_Off(void)
{
	OLED_WR_Byte(0XAE,OLED_CMD);  //关显示
	OLED_WR_Byte(0X8D,OLED_CMD);  //设置OLED电荷泵
	OLED_WR_Byte(0X10,OLED_CMD);  //失能,关
}		   			 


/*
	@brief			清屏
	@param			无
	@retval			无
 */	  
void OLED_Clear(void)  
{  
	u8 i,n;		    //定义变量
	for(i=0;i<8;i++)  
	{  
		OLED_WR_Byte (0xb0+i,OLED_CMD);    //从0~7页依次写入
		OLED_WR_Byte (0x00,OLED_CMD);      //列低地址
		OLED_WR_Byte (0x10,OLED_CMD);      //列高地址  
		for(n=0;n<128;n++)
		{
			OLED_WR_Byte(0,OLED_DATA); //写入 0 清屏
		}
	}
}


/*
	@brief			显示一个字符
	@param			x:起始列
					y:起始页,SIZE = 16占两页;SIZE = 12占1页
					chr:字符
	@retval			无
 */
void OLED_ShowChar(u8 x,u8 y,u8 chr)
{      	
	u8 c=0,i=0;	
	c=chr-' '; //获取字符的偏移量	
	if(x>Max_Column-1){x=0;y=y+2;} //如果列数超出了范围,就从下2页的第0列开始

	if(SIZE ==16) 					//字符大小如果为 16 = 8*16
		{
			OLED_Set_Pos(x,y);	//从x y 开始画点
			for(i=0;i<8;i++)  	//循环8次 占8列
			OLED_WR_Byte(F8X16[c*16+i],OLED_DATA); //找出字符 c 的数组位置,先在第一页把列画完
			OLED_Set_Pos(x,y+1); //页数加1
			for(i=0;i<8;i++)  //循环8次
			OLED_WR_Byte(F8X16[c*16+i+8],OLED_DATA); //把第二页的列数画完
		}
	else 	//字符大小为 6 = 6*8
		{	
			OLED_Set_Pos(x,y+1); //一页就可以画完
			for(i=0;i<6;i++) //循环6次 ,占6列
			OLED_WR_Byte(F6x8[c][i],OLED_DATA); //把字符画完
		}
}


/*
	@brief			计算m^n
	@param			m:输入的一个数
					n:输入数的次方
	@retval			result:一个数的n次方
 */
u16 oled_pow(u8 m,u8 n)
{
	u16 result=1;	 
	while(n--)result*=m;    
	return result;
}				  


/*
	@brief			在指定的位置,显示一个指定长度大小的数
	@param			x:起始列
					y:起始页
					num:数字
					len:数字的长度
					size:显示数字的大小
	@retval			无
 */		  
void OLED_ShowNum(u8 x,u8 y,u16 num,u16 len,u16 size)
{         	
	u8 t,temp;  															//定义变量
	u8 enshow=0;															//定义变量

	for(t=0;t<len;t++)
	{
		temp=(num/oled_pow(10,len-t-1))%10; 		//取出输入数的每个位,由高到低
		if(enshow==0&&t<(len-1)) //enshow:是否为第一个数;t<(len-1):判断是否为最后一个数
		{
			if(temp==0) //如果该数为0 
			{
				OLED_ShowChar(x+(size/2)*t,y,' ');	//显示 0 ;x+(size2/2)*t根据字体大小偏移的列数(8)
				continue; //跳过剩下语句,继续重复循环(避免重复显示)
			}else enshow=1; 
		}
	 	OLED_ShowChar(x+(size/2)*t,y,temp+'0'); //显示一个位;x+(size2/2)*t根据字体大小偏移的列数(8)
	}
} 


/*
	@brief			显示字符串
	@param			x:起始列
					y:起始页
					*chr:第一个字符首地址
	@retval			无
 */
void OLED_ShowString(u8 x,u8 y,u8 *chr)
{
	u8 j=0; //定义变量

	while (chr[j]!='\0') //如果不是最后一个字符
	{		
		OLED_ShowChar(x,y,chr[j]); //显示字符
		x+=8; //列数加8 ,一个字符的列数占8
		if(x>=128){x=0;y+=2;} //如果x大于等于128,切换页,从该页的第一列显示
		j++; //下一个字符
	}
}


/*
	@brief			显示中文
	@param			x:起始列;一个字体占16列
					y:起始页;一个字体占两页
					no:字体的序号
	@retval			无
 */
void OLED_ShowCHinese(u8 x,u8 y,u8 no,u8 w,u8 h)
{      			    
	u8 t,k,addr0=(h/8)*no; //定义变量
  for(k=0;k<h/8;k++)
	{
			OLED_Set_Pos(x,y+k);	//从 x y 开始画点,先画第一页
			for(t=0;t<w;t++) //循环16次,画第一页的16列
			{
				OLED_WR_Byte(FONT_X[addr0][t],OLED_DATA);//画no在数组位置的第一页16列的点
			}
			addr0++;			
	}		
}

/*
	@brief			显示图片
	@param			x0:起始列地址
					y0:起始行地址
					x1:终止列地址
					y1:终止行地址
					BMP[]:存放图片代码的数组
	@retval			无
 */
void OLED_DrawBMP(u8 x0, u8 y0,u8 x1, u8 y1,u8 BMP[])
{ 	
 	u8 j=0; //定义变量
 	u8 x,y; //定义变量
  
 	if(y1%8==0) y=y1/8;       //判断终止页是否为8的整数倍
 	 else y=y1/8+1;

		for(y=y0;y<y1;y++)      //从起始页开始,画到终止页
		{
			OLED_Set_Pos(x0,y);   //在页的起始列开始画
   			for(x=x0;x<x1;x++)  //画x1 - x0 列
	    		{
	    			OLED_WR_Byte(BMP[j++],OLED_DATA);	//画图片的点    	
	    		}
		}
}