基于STM32F103系列单片机的家用燃气监控系统
家用燃气监控系统
一,题目任务要求和具体功能要求
1.1任务要求:
设计基于STM32F103系列单片机的家用燃气监控系统。系统需要实现以下功能:
1. 采集家居厨房环境中的天然气和一氧化碳浓度数据。
2. 当浓度数据超过设定阈值时,进行声光报警提示。
3. 利用定时器输出PWM信号驱动2路舵机摆动:
1路舵机控制燃气开关。
1路舵机控制窗户开关。
4. 通过串口通信将数据传输到远端PC机,并在PC端进行模拟显示和报警提示。
1.2具体功能要求:
1. 设计单片机的最小系统(基本项)
2. 串口通信电路设计,实现与PC机通信(基本项)
3. 按键电路设计(基本项)
4. 燃气浓度采集电路设计(基本项)
5. 通用输入输出程序设计(基本项)
6. 燃气浓度采样程序设计(基本项)
7. 串口通信程序设计(基本项)
8. 定时器PWM程序设计(基本项)
9. 提高测量精度的方法(创新项)
二、设计方案论证
2.1 系统总体架构
系统主要由以下几个部分组成:
传感器模块:包括天然气传感器和一氧化碳传感器,用于采集环境中的燃气浓度数据。
主控模块:基于STM32F103单片机,负责数据处理、判断、控制输出。
报警模块:包括LED指示灯和蜂鸣器,用于声光报警提示。
执行模块:通过PWM信号控制2路舵机,分别控制燃气开关和窗户开关。
通信模块:通过串口通信将数据传输到PC端,实现数据的实时监控和报警提示。
用户接口模块:包括按键,用于系统设置和手动控制。
显示模块:显示天然气传感器(MQ-4)与一氧化碳传感器(MQ-7)的阈值与实时数据。
2.2 主要功能模块设计
2.2.1 单片机最小系统设计
核心器件:STM32F103C8T6单片机。
电源部分:提供稳压电源,确保单片机及各外设正常工作。
时钟部分:外接晶振(如8MHz)提供单片机工作时钟。
复位电路:复位电路确保单片机上电后正常启动。
编程接口:连接下载接口线,实现程序下载与调试。
2.2.2 串口通信电路设计
串口接口:利用STM32的USART接口,通过串口转USB模块与PC机通信。
电平转换:确保串口信号与USB模块的电平匹配,避免信号失真。
连接方式:TXD和RXD分别连接到单片机的对应引脚,GND连接共地。
2.2.3 按键电路设计
按键数量:设置三个按键,一个用于切换控制传感器的阈值,一个用于控制传感器阈值的增大,一个用于控制传感器阈值的减少。
去抖设计:每个按键并联一个电容进行去抖处理,确保按键信号稳定。
接口电路:每个按键一端连接VCC,另一端连接到单片机的GPIO引脚。
2.2.4 燃气浓度采集电路设计
传感器选择:选用高灵敏度的天然气传感器(MQ-4)和一氧化碳传感器(MQ-7)。
接口电路:传感器VCC接口连接到VCC(5V),传感器GND接口接地线,采用AO与单片机GPIO口链接
2.2.5 通用输入输出程序设计
GPIO:配置各个GPIO口的输入输出模式,包括传感器输入、声光报警输出、舵机控制输出等。
2.2.6 燃气浓度采样程序设计
ADC配置:配置ADC模块,设定采样频率和分辨率,便于传感器的浓度采集。
2.2.7 串口通信程序设计
初始化:配置USART的波特率、数据位、停止位和校验方式。
数据发送:将采集到的浓度数据通过串口发送给PC端。
数据接收:根据需要接收来自PC端的控制指令。
2.2.8 定时器PWM程序设计
定时器配置:配置定时器模块以产生PWM信号,控制舵机的角度。
PWM参数:设定PWM频率和占空比,确保舵机的正常驱动。
舵机控制:通过调整PWM信号的占空比,实现舵机的精确定位控制。
2.2.9 提高测量精度的方法(创新项)
采用平均滤波算法来提高采样数据的精确度和减少噪声。
2.3工作原理
家用燃气监控系统通过MQ-4和MQ-7传感器实时检测天然气和一氧化碳浓度,STM32单片机处理传感器数据并判断是否超标。若浓度超标,系统通过蜂鸣器和LED报警,并通过舵机控制燃气阀门和窗户。数据通过串口传输至PC进行远程监控,用户可通过按键调整报警阈值。该系统提供了自动报警和安全控制功能,保障家庭安全。
三、硬件部分设计
3.1硬件部分的组成,器件的选型(主要参数,外部引脚定义和实物外形),器件清单
序号 |
名称 |
型号 |
外部引脚定义及参数 |
实际使用数量 |
实物外形 |
1 |
0.96寸OLED显示屏窄白光IIC |
HS96L03W2C03 |
GND VCC SCL SDA |
|
|
2 |
舵机 |
SG90 |
GND VCC(5V) PWM |
2 |
|
3 |
一氧化碳传感器 |
MQ-7 |
VCC GND DO AO |
1 |
|
4 |
天然气传感器 |
MQ-4 |
VCC GND DO AO |
1 |
|
5 |
通用串口通信模块 |
CH3430 |
GND 5V 3V TXD RXD |
1 |
|
6 |
发光二极管(红色,蓝色) |
|
|
2 |
|
7 |
蜂鸣器 |
TMB12A05 |
GND VCC I/O |
|
|
8 |
电容 |
|
0.1uf |
|
|
9 |
按键 |
|
|
3 |
3.2各部分硬件电路图,总体电路原理
![]() |
|
![]() |
3.3硬件仿真电路图
四、软件部分设计
4.1模块函数
4.1.1 按键控制模块:包括按键初始化,按键获取键码。
include "stm32f10x.h" // Device header
#include "Delay.h"
/**
* 函 数:按键初始化
* 参 数:无
* 返 回 值:无
*/
void Key_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_4| GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA2,PA4和PA6引脚初始化为上拉输入
}
/**
* 函 数:按键获取键码
* 参 数:无
* 返 回 值:按下按键的键码值,范围:0~2,返回0代表没有按键按下
* 注意事项:此函数是阻塞式操作,当按键按住不放时,函数会卡住,直到按键松手
*/
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0; //定义变量,默认键码值为0
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == 0) //读PA2输入寄存器的状态,如果为0,则代表按键1按下
{
Delay_ms(20); //延时消抖
while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == 0); //等待按键松手
Delay_ms(20); //延时消抖
KeyNum = 1; //置键码为1
}
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) == 0) //读PA4输入寄存器的状态,如果为0,则代表按键2按下
{
Delay_ms(20); //延时消抖
while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) == 0); //等待按键松手
Delay_ms(20); //延时消抖
KeyNum = 2; //置键码为2
}
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == 0) //读PA6输入寄存器的状态,如果为0,则代表按键3按下
{
Delay_ms(20); //延时消抖
while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == 0); //等待按键松手
Delay_ms(20); //延时消抖
KeyNum = 3; //置键码为2
}
return KeyNum; //返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0
}
4.1.2 蜂鸣器控制模块:包括蜂鸣器初始化,蜂鸣器开启,蜂鸣器关闭。
#include "stm32f10x.h" // Device header
void Buzzer_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_12);
}
void Buzzer_ON(void)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_12);
}
void Buzzer_OFF(void)
{
GPIO_SetBits(GPIOB, GPIO_Pin_12);
}
void Buzzer_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_12) == 0)
{
GPIO_SetBits(GPIOB, GPIO_Pin_12);
}
else
{
GPIO_ResetBits(GPIOB, GPIO_Pin_12);
}
}
4.1.3 PWM控制模块:包括PWM初始化,PWM设置CCR。
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OC3Init(TIM3, &TIM_OCInitStructure);
TIM_OC4Init(TIM3, &TIM_OCInitStructure);
TIM_Cmd(TIM3, ENABLE);
}
void PWM_SetCompare3(uint16_t Compare)
{
TIM_SetCompare3(TIM3, Compare); //??CCR2??
}
void PWM_SetCompare4(uint16_t Compare)
{
TIM_SetCompare4(TIM3, Compare); //??CCR2??
}
4.1.4 舵机控制模块:包括舵机初始化,舵机设置角度。
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Servo_Init(void)
{
PWM_Init(); }
void Servo_SetAngle0(float Angle1)
{
PWM_SetCompare3(Angle1 / 180 * 2000 + 500);
}
void Servo_SetAngle1(float Angle2)
{
PWM_SetCompare4(Angle2 / 180 * 2000 + 500);
}
4.1.5 LED控制模块:包括LED初始化,LED开启,LED关闭。
#include "stm32f10x.h" // Device header
/**
* 函 数:LED初始化
* 参 数:无
* 返 回 值:无
*/
void LED_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11| GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB11和PB10引脚初始化为推挽输出
/*设置GPIO初始化后的默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_11| GPIO_Pin_10); //设置B11和PB10引脚为高电平
}
/**
* 函 数:LED1开启
* 参 数:无
* 返 回 值:无
*/
void LED1_ON(void)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_11); //设置PA1引脚为低电平
}
/**
* 函 数:LED1关闭
* 参 数:无
* 返 回 值:无
*/
void LED1_OFF(void)
{
GPIO_SetBits(GPIOB, GPIO_Pin_11); //设置PA1引脚为高电平
}
/**
* 函 数:LED1状态翻转
* 参 数:无
* 返 回 值:无
*/
void LED1_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_11) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平
{
GPIO_SetBits(GPIOB, GPIO_Pin_11); //则设置PA1引脚为高电平
}
else //否则,即当前引脚输出高电平
{
GPIO_ResetBits(GPIOB, GPIO_Pin_11); //则设置PA1引脚为低电平
}
}
/**
* 函 数:LED2开启
* 参 数:无
* 返 回 值:无
*/
void LED2_ON(void)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_10); //设置PA2引脚为低电平
}
/**
* 函 数:LED2关闭
* 参 数:无
* 返 回 值:无
*/
void LED2_OFF(void)
{
GPIO_SetBits(GPIOB, GPIO_Pin_10); //设置PA2引脚为高电平
}
/**
* 函 数:LED2状态翻转
* 参 数:无
* 返 回 值:无
*/
void LED2_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_10) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平
{
GPIO_SetBits(GPIOB, GPIO_Pin_10); //则设置PA2引脚为高电平
}
else //否则,即当前引脚输出高电平
{
GPIO_ResetBits(GPIOB, GPIO_Pin_10); //则设置PA2引脚为低电平
}
}
4.1.6O LED控制模块:包括OLED初始化,OLED显示字符串,OLED显示数字,OLED显示汉字,OLED清屏。
#include "stm32f10x.h"
#include "OLED_Font.h"
#include <Stdio.h> //使用Sprintf时要包含
/*引脚配置*/
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))
/*引脚初始化*/
void OLED_I2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //GPIOB
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; // PB8 SCL
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // PB9 SDA
GPIO_Init(GPIOB, &GPIO_InitStructure);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
/**
* @brief I2C开始
* @param 无
* @retval 无
*/
void OLED_I2C_Start(void)
{
OLED_W_SDA(1);
OLED_W_SCL(1);
OLED_W_SDA(0);
OLED_W_SCL(0);
}
/**
* @brief I2C停止
* @param 无
* @retval 无
*/
void OLED_I2C_Stop(void)
{
OLED_W_SDA(0);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
/**
* @brief I2C发送一个字节
* @param Byte 要发送的一个字节
* @retval 无
*/
void OLED_I2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
OLED_W_SDA(Byte & (0x80 >> i));
OLED_W_SCL(1);
OLED_W_SCL(0);
}
OLED_W_SCL(1); //额外的一个时钟,不处理应答信号
OLED_W_SCL(0);
}
/**
* @brief OLED写命令
* @param Command 要写入的命令
* @retval 无
*/
void OLED_WriteCommand(uint8_t Command)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x00); //写命令
OLED_I2C_SendByte(Command);
OLED_I2C_Stop();
}
/**
* @brief OLED写数据
* @param Data 要写入的数据
* @retval 无
*/
void OLED_WriteData(uint8_t Data)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x40); //写数据
OLED_I2C_SendByte(Data);
OLED_I2C_Stop();
}
/**
* @brief OLED设置光标位置
* @param Y 以左上角为原点,向下方向的坐标,范围:0~7
* @param X 以左上角为原点,向右方向的坐标,范围:0~127
* @retval 无
*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
OLED_WriteCommand(0xB0 | Y); //设置Y位置
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位
OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}
/**
* @brief OLED清屏
* @param 无
* @retval 无
*/
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++)
{
OLED_SetCursor(j, 0);
for(i = 0; i < 128; i++)
{
OLED_WriteData(0x00);
}
}
}
/**
* @brief OLED显示一个字符
* @param Line 行位置,范围:1~4
* @param Column 列位置,范围:1~16
* @param Char 要显示的一个字符,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //设置光标位置在上半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容
}
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //显示下半部分内容
}
}
/**
* @brief OLED显示字符串
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++)
{
OLED_ShowChar(Line, Column + i, String[i]);
}
}
/**
* @brief OLED次方函数
* @retval 返回值等于X的Y次方
*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
/**
* @brief OLED显示数字(十进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~4294967295
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十进制,带符号数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-2147483648~2147483647
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
}
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十六进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFFFFFF
* @param Length 要显示数字的长度,范围:1~8
* @retval 无
*/
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++)
{
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10)
{
OLED_ShowChar(Line, Column + i, SingleNumber + '0');
}
else
{
OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
}
}
}
/**
* @brief OLED显示数字(二进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
}
}
/**
* @brief OLED显示波形函数
* @param Wave_sum波形值,范围:0~15
* @retval 无
*/
void OLED_Wave(u8 Wave_Sum)//波形刷新
{
static u8 i;
OLED_WriteCommand (0xb6); //设置页地址
OLED_WriteCommand((i & 0x0f)); //设置显示位置—列低地址
OLED_WriteCommand(((i & 0xf0) >> 4) |0x10); //设置显示位置—列高地址
OLED_WriteData(PPG_Wave_Fig[Wave_Sum*2]);
OLED_WriteCommand (0xb7); //设置页地址
OLED_WriteCommand ((i & 0x0f)); //设置显示位置—列低地址
OLED_WriteCommand (((i & 0xf0) >> 4) |0x10); //设置显示位置—列高地址
OLED_WriteData(PPG_Wave_Fig[Wave_Sum*2+1]); //显示数据,PPG_wave_fig[n]的值跟OLED寄存器配置的扫描方式相关
i++;
if(i>127) i=0;
}
/**
* @brief OLED显示中文
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~8
* @param ChineseLoca 中文字符位置,范围:自定义
* @retval 无
*/
void OLED_ShowChinese(uint8_t Line,uint8_t Column,uint8_t ChineseLoca)
{
uint8_t i = 0;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 16); //设置光标位置在上半部分
for (i = 0; i < 16; i++)
{
OLED_WriteData(Hzk[2*ChineseLoca][i]); //显示上半部分内容
}
OLED_SetCursor((Line-1) * 2+1, ((Column - 1) * 16)); //设置光标位置在下半部分
for (i = 0; i < 16; i++)
{
OLED_WriteData(Hzk[2*ChineseLoca+1][i]); //显示下半部分内容
}
}
void OLED_ShowFloat(uint8_t Line, uint8_t Column,float Parameter) //把可变参数Parameter以小数类型显示
{
char buf[20];
sprintf(buf,"%.2f",Parameter); //保留两位小数,并以逐个单字符存入数组buf中,包括小数点 Parameter=buf
OLED_ShowString(Line, Column, buf); //在line行Column列输出小数Parameter
}
/**
* @brief OLED初始化
* @param 无
* @retval 无
*/
void OLED_Init(void)
{
uint32_t i, j;
for (i = 0; i < 1000; i++) //上电延时
{
for (j = 0; j < 1000; j++);
}
OLED_I2C_Init(); //端口初始化
OLED_WriteCommand(0xAE); //关闭显示
OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); //设置多路复用率
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); //设置显示偏移
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); //设置显示开始行
OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
OLED_WriteCommand(0xDA); //设置COM引脚硬件配置
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); //设置对比度控制
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); //设置预充电周期
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); //设置整个显示打开/关闭
OLED_WriteCommand(0xA6); //设置正常/倒转显示
OLED_WriteCommand(0x8D); //设置充电泵
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); //开启显示
OLED_Clear(); //OLED清屏
}
4.1.7 AD采样模块:包括ADC初始,获取ADC转换值等。
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "math.h"
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void Adc_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE );
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
}
u16 Get_Adc(u8 ch)
{
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_71Cycles5 );
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));
return ADC_GetConversionValue(ADC1);
}
4.1.8 采样数据处理模块:
u16 Get_Adc_Average(u8 ch,u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_Adc(ch);
Delay_ms(5);
}
return temp_val/times;
}
4.1.9 传感器收集模块:
float MQ_4_work(void)
{
u16 ad = 0;
float mqvalue,ppm;
ad=Get_Adc_Average(ADC_Channel_1,10);
mqvalue = 3.3*ad/4095;
ppm=mqvalue/0.1*200;
//OLED_ShowFloat(1,8,mqvalue);
OLED_ShowNum(2,8,ppm,4);
//OLED_ShowChinese(1,1,2);
//OLED_ShowChinese(1,2,3);
//OLED_ShowChinese(1,3,4);
//OLED_ShowString(1,7,":");
OLED_ShowString(2,1,"ppm :");
Delay_ms(500);
return ppm;
}
float MQ_7_work(void)
{
u16 ad = 0;
float mqvalue,ppm;
ad=Get_Adc_Average(ADC_Channel_0,10);
mqvalue = 5*ad/4095;
float RS=(5-mqvalue)/(mqvalue*0.5);
float Ro=6.64;
ppm = pow(11.5428*Ro/RS,0.6549f);
//OLED_ShowFloat(1,8,mqvalue);
OLED_ShowNum(4,8,ppm,4);
//OLED_ShowChinese(1,1,2);
//OLED_ShowChinese(1,2,3);
//OLED_ShowChinese(1,3,4);
//OLED_ShowString(1,7,":");
OLED_ShowString(4,1,"ppm :");
Delay_ms(50);
return ppm;
}
4.1.10 串口通信控制模块:包括串口初始化,串口发送数字,串口发送字符串,串口中断。
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
char Serial_RxPacket[100];
uint8_t Serial_RxFlag;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)
{
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if (RxState == 0)
{
if (RxData == '@' && Serial_RxFlag == 0)
{
RxState = 1;
pRxPacket = 0;
}
}
else if (RxState == 1)
{
if (RxData == '\r')
{
RxState = 2;
}
else
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
}
}
else if (RxState == 2)
{
if (RxData == '\n')
{
RxState = 0;
Serial_RxPacket[pRxPacket] = '\0';
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
4.2主函数:
1头文件定义:引入所有必要的模块头文件,以支持各功能模块的操作。
2. 初始化:程序开始时,初始化OLED显示屏、ADC(模数转换器)、舵机、按键、LED灯、蜂鸣器和串口通信。
3. 主循环:
读取按键状态,并根据按键操作调整报警阈值。
读取MQ-4和MQ-7传感器的值。
在OLED显示屏上显示当前的阈值和传感器值。
如果传感器值超过阈值,蜂鸣器响起,LED灯亮起,并通过串口发送数据。
如果两个传感器的值都低于阈值,舵机转动到180度。
4. 按键操作:
按键1用于切换调整的阈值(甲烷或一氧化碳)。
按键2和按键3用于增加或减少当前选择的阈值。
5. 传感器数据读取:
`MQ_4_work()`函数用于读取MQ-4传感器的值。
MQ_7_work()`函数用于读取MQ-7传感器的值。
6. 反馈机制:
当MQ-4传感器检测到的甲烷浓度超过阈值时,蜂鸣器响起,LED2灯亮起,舵机转动到90度,并通过串口发送数据。
当MQ-7传感器检测到的一氧化碳浓度超过阈值时,蜂鸣器响起,LED1灯亮起,舵机转动到90度,并通过串口发送数据。
7. 串口通信:
通过`Serial_SendNumber()`和`Serial_Printf()`函数将传感器数据发送到串口。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "OLED.h"
#include "AD.h"
#include "KEY.h"
#include "Servo.h"
#include "Buzzer.h"
#include "Serial.h"
uint16_t AD0, AD1; //定义AD值变量
uint8_t KeyNum; //定义用于接收键码的变量
float Angle1; //定义角度变量
float Angle2; //定义角度变量
float taq=0;
float co=0;
float taq1=100;
float co1=100;
uint8_t KEY=0;
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Adc_Init(); //AD初始化
Servo_Init(); //舵机初始化
Key_Init(); //按键初始化
LED_Init();
Buzzer_Init();
OLED_ShowString(1, 1, "MQ-4:");
OLED_ShowString(3, 1, "MQ-7:");
Serial_Init();
while (1)
{
KeyNum = Key_GetNum(); //获取按键键码
if (KeyNum == 1) //按键1按下
KEY=KEY+KeyNum;
if (KEY%2==0)
{if (KeyNum==2)
{
taq1+=100;
}
if (KeyNum==3)
{
taq1-=100;
}
}
if (KEY%2!=0)
{if (KeyNum==2)
{
co1+=100;
}
if (KeyNum==3)
{
co1-=100;
}
}
taq = MQ_4_work();
co = MQ_7_work();
OLED_ShowNum(1,8,taq1,4);
OLED_ShowNum(3,8,co1,4);
if(taq>=taq1)
{Angle1=90;
Buzzer_ON();
LED2_ON();
Servo_SetAngle0(90);//设置舵机的角度为角度变量
Servo_SetAngle1(90);//设置舵机的角度为角度变量
Delay_ms(100);
Delay_ms(100);
Buzzer_OFF();
LED2_OFF();
Serial_SendNumber(taq, 4);
Serial_Printf("\r\n");
}
if(co>=co1)
{Angle2=90;
Buzzer_ON();
LED1_ON();
Servo_SetAngle0(90);//设置舵机的角度为角度变量
Servo_SetAngle1(90);//设置舵机的角度为角度变量
Delay_ms(100);
Buzzer_OFF();
Serial_SendNumber(co, 4);
Serial_Printf("\r\n");
LED1_OFF();}
if(taq<taq1&co<co1)
{Servo_SetAngle1(180);//设置舵机的角度为角度变量
Servo_SetAngle0(180);//设置舵机的角度为角度变量
}
}
}
五,测试及调试
5.1 硬件测试方法
5.1.1 电源系统测试方法
使用万用表对系统各供电点进行测量,包括3.3V和5V供电轨的电压值。同时使用示波器观察电源纹波情况,测量电源纹波峰峰值。在不同负载条件下测试电源的稳定性,包括空载、满载和负载突变情况下的电压变化情况。
5.1.2 传感器模块测试方法
对MQ-4和MQ-7传感器进行标准预热程序,使用标准气体进行传感器校准。在通风环境下,使用不同浓度的标准气体测试传感器的线性度和重复性。记录传感器从检测到响应的时间,验证传感器的灵敏度和响应速度是否满足系统要求。
5.1.3 舵机控制测试方法
使用示波器测量PWM信号的频率和占空比,确保信号稳定性。测试舵机在不同角度位置的控制精度,包括0度、90度和180度等关键位置。检查舵机在频繁动作时的发热情况和工作稳定性。
5.2 软件测试方法
5.2.1 按键测试方法
通过多种按键操作方式测试按键的可靠性,包括短按、长按和快速连续按压等情况。记录按键的抖动情况,测试消抖程序的效果。验证按键在不同操作速度下的响应情况,确保按键操作的稳定性。
5.2.2 传感器数据采集测试方法
使用示波器监测ADC采样的时序,验证采样频率是否符合设计要求。进行多次采样测试,分析采样数据的稳定性和可靠性。测试ADC在不同输入信号下的转换精度,确保数据采集的准确性。
5.2.3 串口通信测试方法
使用串口调试助手进行数据收发测试,验证不同波特率下的通信质量。测试系统在持续数据传输时的稳定性,模拟通信中断情况下的系统响应。检查数据传输的准确性和实时性,确保通信协议的可靠实现。
5.2.4 报警功能测试方法
通过改变传感器输入,测试系统在不同浓度数据下的报警响应。验证报警触发和解除的阈值设置是否准确,测试报警器件(LED和蜂鸣器)的工作状态。检查报警信息的显示和传输是否正确。
5.3. 系统整体测试方法
5.3.1 功能测试方法对系统所有功能模块进行完整性测试,包括传感器采集、数据处理、显示输出、报警控制等。验证各个模块之间的配合是否协调,确保系统功能的完整性和可靠性。
5.3.2 稳定性测试方法
设置系统连续运行测试,监控系统的各项参数变化。记录系统运行过程中的异常情况,包括死机、重启等。检查内存使用情况,确保没有内存泄漏问题。
5.3.3 异常处理测试方法
模拟各种可能的故障情况,如断电、传感器失效、通信中断等。测试系统的故障检测和处理能力,验证系统的自我保护功能是否有效。
5.4. 遇到的问题与解决方法
5.4.1 电路设计问题与解决方法
舵机驱动时出现动力不足问题,重新编写的PWM代码,选择5V为VCC。
5.4.2 引脚选择问题与解决方法
初始PWM引脚选择与串口引脚发生冲突。将舵机控制PWM信号改用定时器3的通道1(PB0)和通道2(PB1),避开了与串口的冲突。
5.4.3 虚焊问题与解决方法
传感器接口处出现虚焊现象,导致数据不稳定。重新使用高质量焊锡进行焊接,并使用万用表测量确保连接可靠。
5.4.4 代码结构设计问题与解决方法
代码中存在重复逻辑,造成维护困难。通过将相似功能封装成独立函数,如报警处理和传感器数据采集函数,提高了代码复用性和可维护性。
5.5. 调试结果
系统功能全部达到设计要求
传感器响应时间小于2秒
串口通信稳定,波特率9600
舵机控制精度在±2°以内
系统连续运行72小时无异常
六,总结
在这项基于STM32F103单片机的家用燃气监控系统设计中,主要涉及了嵌入式系统的各个方面。以下是我从这个项目中总结和掌握的知识和技能:
1. 嵌入式系统硬件设计
单片机的最小系统设计:熟悉STM32F103的最小系统设计,了解了如何连接电源、时钟、复位电路等基本组件。
传感器接口:掌握了如何通过模拟接口读取传感器数据,特别是MQ-4和MQ-7的气体浓度传感器。
舵机控制:学习了如何利用PWM信号控制舵机的角度,通过定时器配置和调节PWM占空比实现精确控制。
2. 软件开发技能
GPIO与定时器编程:了解了如何使用GPIO口进行输入输出操作,以及如何配置定时器产生PWM信号,驱动外部设备。
串口通信:掌握了如何通过串口与PC进行数据通信,使用串口调试助手测试数据传输的准确性和实时性。
按键扫描与去抖动:设计了按键电路并实现了去抖动处理,确保按键操作的稳定性。
数据采集与显示:使用ADC模块采集传感器数据并通过OLED屏幕实时显示,学习了如何通过程序设计来处理和展示传感器数据。
3. 调试与测试技能
电路和硬件调试:通过使用万用表和示波器进行电源电压、电流和信号质量的测试,确保硬件的稳定性和可靠性。
软件调试:通过串口调试、逻辑分析等手段测试软件的稳定性和功能的实现,确保系统在各种条件下都能正常运行。
性能优化:对系统进行性能评估,监控CPU和内存的使用,优化代码结构,确保系统的高效运行。
4. 整体系统设计与优化
系统整体架构设计:从系统功能需求出发,设计了数据采集、报警控制、舵机驱动、串口通信等模块,并实现了模块间的协调工作。
异常处理与容错设计:模拟了传感器故障、电源问题、通信中断等异常情况,设计了合适的异常处理机制,增强了系统的鲁棒性。
5. 团队协作与项目管理
团队合作:在项目中分工明确,充分利用团队成员的专业特长,共同解决系统设计中的难题。
文档编写与报告:通过编写详细的测试报告、调试记录和总结报告,锻炼了文档写作与项目管理的能力。
通过本项目,我不仅掌握了嵌入式系统的硬件与软件设计,也对嵌入式系统的调试、优化、以及团队合作有了更深入的理解。这些经验将为我未来在嵌入式系统开发方面打下坚实的基础。
作者:月夕24