智能家居系统(基于STM32F103C8T6标准库+FreeRTOS+Qt串口开发实现)
视频演示:基于STM32F103C8T6标准库+FreeRTOS+Qt串口开发实现的智能家居项目_哔哩哔哩_bilibili
基于STM32F103C8T6标准库+FreeRTOS+Qt串口开发实现的智能家居项目: https://pan.baidu.com/s/1f41gAfOOnlcQoKoMx3o84A?pwd=6j2g 提取码: 6j2g
注:本项目为学习完《江科大STM32教程》实战项目。受限于个人水平,还有很多需要改进的地方,还请读者见谅,如有不当之处,还请指证。
01 项目需求
1.主控:STM32F103C8T6;
2.按键+LED:模拟开关灯,支持本地控制和上位机控制;
3.按键+直流电机驱动模块+直流电机+风扇叶片:模拟降温系统,支持本地控制和上位机控制,本地控制实现直流电机转或不转,上位机控制带调速功能;
4.按键+无源蜂鸣器:模拟音乐播放系统,支持本地控制和上位机控制;
5.DHT11温湿度传感器:测量温湿度并上报;
6.BH1750光照传感器:测量光照强度并上报;
7.OLED显示屏:显示温湿度、光照强度、音乐播放状态;
8.Qt串口上位机:Qt上位机实现,LED、风扇、音乐播放控制以及温湿度采集。
02 硬件连接
注意:本电路仅体现个模块之间的电路连接。原理图绘制参看:
嘉立创EDA使用流程
本来一开始想用ESP8266使用mQTT协议进行远程控制的,不过在做项目过程中,ESP8266似乎被玩坏了,所以临时改用串口控制,使用STM32的串口1,上图中ESP8266模块就没用到了,后期看情况更新MQTT版本。
03 驱动部分编写
3.1 照明系统
功能:实现本地按键开关灯,远程开关灯。本节先实现本地任务。
硬件:按键+LED
因为测量温度、亮度、播放音乐等任务执行后一直进行,所以需要使用中断来处理开关灯任务。
3.1.1 LED驱动
LED.c
/**
*Encoding:GB2312
*文件功能:LED开关灯功能实现
**/
#include "stm32f10x.h" // Device header
/**
* 函 数:LED初始化,LED接在PC13上
* 参 数:无
* 返 回 值:无
*/
void LED_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); //开启GPIOC的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure); //将PC13引脚初始化为推挽输出
/*设置GPIO初始化后的默认电平*/
GPIO_SetBits(GPIOC, GPIO_Pin_13); //设置PC13引脚为高电平,不亮,因为我们的引脚正极接高电平,负极接PC13
}
/**
* 函 数:LED1开启
* 参 数:无
* 返 回 值:无
*/
void LED_ON(void)
{
GPIO_SetBits(GPIOC, GPIO_Pin_13); //设置PC13引脚为低电平
}
/**
* 函 数:LED1关闭
* 参 数:无
* 返 回 值:无
*/
void LED_OFF(void)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13); //设置PC13引脚为高电平
}
/**
* 函 数:LED状态翻转
* 参 数:无
* 返 回 值:无
*/
void LED_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平
{
GPIO_SetBits(GPIOC, GPIO_Pin_13); //则设置PC13引脚为高电平
}
else //否则,即当前引脚输出高电平
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13); //则设置PC13引脚为低电平
}
}
LED.h
#ifndef __LED_H
#define __LED_H
void LED_Init(void);
void LED_ON(void);
void LED_OFF(void);
void LED_Turn(void);
#endif
3.1.2 LED按键驱动
这个项目中一共使用了3个按键,本章先给出LED按键驱动的代码,最后会整合到一起去。按键接在PC15引脚上。
Key1.c
/**
*Encoding:GB2312
*文件功能:按键1功能实现
**/
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
/**
*函数:LED按键1初始化函数
*参数:无
*返回值:无
**/
void Key1_Init(void)
{
/*第1步:打开时钟外设*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); //开启GPIOC的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO的时钟
/*EXTI和NVIC的时钟一直是打开的,不需要开启:
EXTI作为一个独立外设,按理说应该是需要开启时钟的但是寄存器里没有EXTI时钟的控制位
NVIC是内核的外设,内核的外设是不需要开启时钟的
RCC管的都是内核外的外设*/
/*第2步:配置GPIO*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure); //将PC15引脚初始化为上拉输入
/*第3步:配置AFIO:
它的库函数是和GPIO在一个文件里的*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource15);
/*第4步:配置EXTI*/
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line15; //指定中断线为第15个线路
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //指定中断线的模式为中断触发
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //指定中断触发方式为上升沿和下降沿均触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE; //指定选择的中断线的新状态
EXTI_Init(&EXTI_InitStruct);
/*第5步:配置NVIC
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,在misc.h文件里*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/*设置优先级分组方式,需要注意的是,这个分组方式整个芯片只能用一种,所以这个分组代码整个工程只需要
执行依次就行了,如果把它放在模块里面进行分组,要确保每个模块分组都是选的同一个,也可以把这个分组代码
放在主函数的最开始,这样模块里就不用在再进行分组了*/
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn; //指定中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //将抢占优先级设置为1,优先级是在多个中断源同时申请,产生拥挤的时候才有作用
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //将响应优先级设置为1
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_Init(&NVIC_InitStruct);
}
/*中断函数:中断函数的名字是固定的,在"startup_stm32f10x_md.s"文件里查看*/
void EXTI15_10_IRQHandler(void) //中断函数的名字都是固定的,并且无参无返回值
{
if (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_15) == 0) //读PC15输入寄存器的状态,如果为0,则代表按键1按下
{
LED_Turn(); //LED翻转
}
EXTI_ClearITPendingBit(EXTI_Line15); //将通道15中断标志位清除
}
Key1.h
#ifndef __KEY_H
#define __KEY_H
void Key1_Init(void);
#endif
通过以上代码,可以实现按键开关灯。需要改进的是,这里按键上升沿下降沿都能触发,因为是在中断函数里实现的LED状态翻转,所以没有进行消抖。
3.2 降温系统
功能:实现本地能够按键开关风扇,远程开关风扇+调速;高于/低于温度阈值自动开启/关闭风扇。本节先实现本地按键开关风扇。
硬件:按键+直流电机驱动模块+直流电机+风扇叶片
3.2.1 直流电机驱动
电路连接,参考江科大教程:
因为要实现电机的调速功能,所以需要使用PWM驱动。
PWM.c
#include "stm32f10x.h" // Device header
/**
* 函 数:PWM初始化
* 参 数:无
* 返 回 值:无
*/
void PWM_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA2引脚初始化为复用推挽输出
//受外设控制的引脚,均需要配置为复用模式
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*输出比较初始化*/
TIM_OCInitTypeDef TIM_OCInitStructure; //定义结构体变量
TIM_OCStructInit(&TIM_OCInitStructure); //结构体初始化,若结构体没有完整赋值
//则最好执行此函数,给结构体所有成员都赋一个默认值
//避免结构体初值不确定的问题
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式,选择PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性,选择为高,若选择极性为低,则输出高低电平取反
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
TIM_OCInitStructure.TIM_Pulse = 0; //初始的CCR值
TIM_OC2Init(TIM2, &TIM_OCInitStructure); //将结构体变量交给TIM_OC3Init,配置TIM2的输出比较通道3
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/**
* 函 数:PWM设置CCR
* 参 数:Compare 要写入的CCR的值,范围:0~100
* 返 回 值:无
* 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
* 占空比Duty = CCR / (ARR + 1)
*/
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare); //设置CCR3的值
}
PWM.h
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare2(uint16_t Compare);
#endif
Motor.c
/**
*Encoding:GB2312
*文件功能:直流电机驱动功能实现
**/
#include "stm32f10x.h" // Device header
#include "PWM.h"
#include "stdbool.h"
bool MotorState; //定义一个全局变量用来记录电机的状态
/**
*函数:直流电机驱动模块初始化函数
*参数:无
*返回值:无
**/
void Motor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
PWM_Init();
}
/**
*函数:直流电机转速设置函数
*参数:Speed设置电机转速,返回state电机状态
*返回值:返回state电机状态:转/不转
**/
bool Motor_SetSpeed(int8_t Speed)
{
if (!MotorState)
{
if (Speed >= 0)
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare2(Speed);
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare2(-Speed);
}
MotorState = true;
}
return MotorState; //如果风扇处于转动过程中返回真,用来处理中断状态翻转
}
Motor.h
#ifndef __MOTOR_H
#define __MOTOR_H
#include "stdbool.h"
void Motor_Init(void);
bool Motor_SetSpeed(int8_t Speed);
#endif
3.2.2 风扇电机按键驱动
Key2.c
/**
*Encoding:GB2312
*文件功能:按键功能实现
**/
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Key2.h"
#include "Motor.h"
#include "stdbool.h"
extern bool MotorState;
/**
*函数:风扇按键2初始化函数
*参数:无
*返回值:无
**/
void Key2_Init(void)
{
/*第1步:打开时钟外设*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO的时钟
/*EXTI和NVIC的时钟一直是打开的,不需要开启:
EXTI作为一个独立外设,按理说应该是需要开启时钟的但是寄存器里没有EXTI时钟的控制位
NVIC是内核的外设,内核的外设是不需要开启时钟的
RCC管的都是内核外的外设*/
/*第2步:配置GPIO*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB1引脚初始化为上拉输入
/*第3步:配置AFIO:
它的库函数是和GPIO在一个文件里的*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
/*第4步:配置EXTI*/
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line1; //指定中断线为第1个线路
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //指定中断线的模式为中断触发
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //指定中断触发方式为上升沿和下降沿均触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE; //指定选择的中断线的新状态
EXTI_Init(&EXTI_InitStruct);
/*第5步:配置NVIC
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,在misc.h文件里*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/*设置优先级分组方式,需要注意的是,这个分组方式整个芯片只能用一种,所以这个分组代码整个工程只需要
执行依次就行了,如果把它放在模块里面进行分组,要确保每个模块分组都是选的同一个,也可以把这个分组代码
放在主函数的最开始,这样模块里就不用在再进行分组了*/
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI1_IRQn; //指定中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //将抢占优先级设置为1,优先级是在多个中断源同时申请,产生拥挤的时候才有作用
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
//将响应优先级设置为1
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_Init(&NVIC_InitStruct);
}
void EXTI1_IRQHandler(void)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //读PB1输入寄存器的状态,如果为0,则代表按键2按下
{
if(MotorState)
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
//按键按下后,如果原来是转的,那么设置PWM为0,电机停止转动
MotorState = false;
}
else
{
Motor_SetSpeed(60); //按下按键后,如果电机原来不转,设置转速默认为60,电机开始转动
}
}
EXTI_ClearITPendingBit(EXTI_Line1); //将通道1中断标志位清除
}
Key.h
#ifndef __KEY_H
#define __KEY_H
void Key2_Init(void);
#endif
通过以上文件,我们能够实现按键控制直流电机转或不转,当然因为在中断里面使用按键,所以也没有实现避免按键消抖的问题。
3.3 音乐播放系统
功能:实现本地能够按键开始或停止播放音乐,远程开始或停止播放音乐。本节先实现本地任务。
硬件:按键+无源蜂鸣器
3.3.1 驱动蜂鸣器实现音乐播放功能
参考连接:
STM32+无源蜂鸣器播放音乐参考
这里我直接使用了以上up的源代码。
Buzzer.c
/**
*Encoding:GB2312
*文件功能:蜂鸣器硬件功能实现
**/
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
*函数:蜂鸣器所使用外设初始化函数
*参数:无
*返回值:无
**/
void Music_init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //将PA0引脚初始化为复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure); //受外设控制的引脚,均需要配置为复用模式
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 99;
TIM_TimeBaseInitStructure.TIM_Prescaler = 1439;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 50;
TIM_TimeBaseInit(TIM2, &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 = 50; //CCR
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
/**
*函数:蜂鸣器频率设置
*参数:无
*返回值:无
**/
void Sound_SetHZ(uint16_t a)
{
TIM_PrescalerConfig(TIM2,a,TIM_PSCReloadMode_Immediate);
}
/**
*函数:播放音乐
*参数:无
*返回值:无
**/
void Play_Music(int a,int b,int c)
{
Music_init();
Sound_SetHZ(a);Delay_ms(b);Sound_SetHZ(20);Delay_ms(c);
}
/**
*函数:暂停音乐
*参数:无
*返回值:无
**/
void Stop_Music(void)
{
Play_Music(20,10,10); //将蜂鸣器频率改为0,实现停止功能
}
Buzzer.h
#ifndef __PLAYMUSIC_H__
#define __PLAYMUSIC_H__
void Music_init(void);
void Sound_SetHZ(uint16_t a);
void Play_Music(int a,int b,int c);
void Stop_Music(void);
#endif
Music.c
/**
*Encoding:GB2312
*文件功能:音乐播放软件功能实现
**/
#include "stm32f10x.h" // Device header
#include "Buzzer.H"
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
uint8_t MusicState = 0; // 定义一个全局变量来记录音乐播放的状态
int i,time,j,a[]={ //音调
1635,1376, 917,1376,2062,1836,1635, 917,1376,1376,1836, 917,1376,1376,
917,1456, 917,1635,1376, 917,1376,2062,1836,1635, 917,1376,1376,1836,
917,1376,1376, 917,1836,1376, 917, //前奏 34
30, 917, 917,1376,1376,1226,1092, //故事的小黄花 41
30, 917, 917,1376,1376,1226,1092,1226,1376,1836, //从出生那年就飘着 51
30, 917, 917,1376,1376,1226,1092, //童年的荡秋千 58
30,1092,1226,1092,1031,1092,1226,1031,1092,1226,1376, //随记忆一直晃到现在 69
1836,1376,1376,1092,1031,1092,1226, //Re So So Si Do Si La 76
1376,1226,1092,1092,1092,1092,1226,1092,1226,1376, //So La Si Si Si Si La Si La So 86
1836,1376,1226,1092,1031,1092,1226,1376, //吹着前奏 望着天空 94
1226,1092,1092,1092,1092,1226,1092,1226,1376, //我想起花瓣试着掉落 103
30,1376,1376,1376,1376,1635,1456,1376, 917,1031,1092,1376,1367,//没想到 失去的勇气我还留着 116
30,1376,1376,1376,1376,1092,1376, //好想再问一遍 123
1635,1456,1376, 917,1031,1092,1376,1226, //你会等待还是离开 131
1092,1226,1031,1092,1376, 917, 728, 687, 728, 917,1376, //刮风这天 我试过握着你手 142
30,1376, 817, 817, 30, 817, 917, 917, //但偏偏 雨渐渐 150
30, 917,1031,1092,1226,1092,1031,1092, //大到我看你不见 158
30,1092,1031, 917,1092, 30,1031, 917, 728, 612, 728, 687, 687, //还要多久 我才能在你身边 171
30, 687, 687, 917, 917, 817, 917,1031, //等到放晴的那天 179
1226,1092, 1031, 917, 817,1376,817, 728, 728, //也许我会比较好一点 188
1092,1226,1031,1092, 30,1376, 917, 728, 687, 728, 917,1376, //从前从前 有个人爱你很久 200
30,1376, 817, 817, 30, 817, 917, 917, //但偏偏 风渐渐 208
30, 917,1031,1092,1226,1092,1031,1092, //大到我看你不见 216
30,1092,1031, 917,1092, 30,1031, 917, 728, 612, 728, 687, 687, //好不容易 又能再多爱一天 229
30,1376, 687, 917, 917, 817, 917, //但故事的最后 236
1031,1635,1456,1376,1226,1092,1226,30,1092,1376 //你好像还是说了 拜拜 246
};
int tm[]={ //音调时长
50, 50, 50, 50, 50, 25, 25, 50, 50, 50, 50, 50, 50, 50,
50, 50, 50, 50, 50, 50, 50, 50, 25, 25, 50, 50, 50, 50,
50, 50, 50, 50, 25, 25, 50, //前奏
50, 50, 50, 50,100, 50, 50, //故事的小黄花
50, 50, 50, 50, 50, 25, 25, 25, 25, 50, //从出生那年就飘着
50, 50, 50, 50,100, 50, 50, //童年的荡秋千
50,100, 25, 25, 25, 25, 25, 25, 25, 25, 50, //随记忆一直晃到现在
50, 50, 50, 50, 50, 50, 50, //Re So So Si Do Si La
25, 25, 50, 50, 50, 50, 25, 25, 50,100, //So La Si Si Si Si La Si La So
50, 50, 50, 50, 50, 50, 50, 25, //吹着前奏 望着天空
25, 50, 50, 50, 50, 25, 25, 50, 75, //我想起花瓣试着掉落
400, 25, 25, 25, 25, 50, 50, 50, 50, 50, 50, 50,125,//没想到 失去的勇气我还留着
50, 25, 25, 25, 25, 50, 50, //好想再问一遍
50, 50, 50, 50, 50, 50, 50,130, //你会等待还是离开
50, 50, 50,100, 50, 50, 50, 50, 50, 50, 50, //刮风这天 我试过握着你手
50, 50, 50, 50, 50, 50, 50, 50, //但偏偏 雨渐渐
50, 50, 50, 50, 50, 50, 50,125, //大到我看你不见
75, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50,125,//还要多久 我才能在你身边
50, 50, 50, 50, 50, 50, 50, 50, //等到放晴的那天
50, 50, 50, 50, 50, 50, 75, 25,100, //也许我会比较好一点
50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, //从前从前 有个人爱你很久
50, 50, 50, 50, 50, 50, 50, 50, //但偏偏 风渐渐
50, 50, 50, 50, 50, 50, 50,150, //大到我看你不见
50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50,125,//好不容易 又能再多爱一天
50, 50, 50, 50, 50, 50, 50, //但故事的最后
50, 50, 50, 50, 50, 50,100, 20, 50,200, //你好像还是说了 拜拜
};
/**
* 函 数:音乐播放
* 参 数:无
* 返 回 值:无
*/
void B_Music(void)
{
MusicState++;
int c;
for(i=0;i<=246;i++)
{
c=5;j=tm[i]/25;
time=j*180;
if(i==49||i==67||i==178)
{
c=0;
}
Play_Music(a[i],time,c);
}
}
Music.h
#ifndef __SOUND_H__
#define __SOUND_H__
void B_Music(void);
#endif
3.3.2 蜂鸣器按键功能实现
Key3.c
/**
*Encoding:GB2312
*文件功能:按键3功能实现
**/
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Key.h"
#include "LED.h"
#include "Buzzer.h"
#include "Music.h"
#include "stdio.h"
#include "usart1.h"
extern uint8_t MusicState;
/**
*函数:蜂鸣器按键3初始化函数
*参数:无
*返回值:无
**/
void Key3_Init(void)
{
/*第1步:打开时钟外设*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO的时钟
/*EXTI和NVIC的时钟一直是打开的,不需要开启:
EXTI作为一个独立外设,按理说应该是需要开启时钟的但是寄存器里没有EXTI时钟的控制位
NVIC是内核的外设,内核的外设是不需要开启时钟的
RCC管的都是内核外的外设*/
/*第2步:配置GPIO*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0引脚初始化为上拉输入
/*第3步:配置AFIO:
它的库函数是和GPIO在一个文件里的*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource7);
/*第4步:配置EXTI*/
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line7; //指定中断线为第0个线路
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //指定中断线的模式为中断触发
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //指定中断触发方式为上升沿和下降沿均触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE; //指定选择的中断线的新状态
EXTI_Init(&EXTI_InitStruct);
/*第5步:配置NVIC
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,在misc.h文件里*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/*设置优先级分组方式,需要注意的是,这个分组方式整个芯片只能用一种,所以这个分组代码整个工程只需要
执行依次就行了,如果把它放在模块里面进行分组,要确保每个模块分组都是选的同一个,也可以把这个分组代码
放在主函数的最开始,这样模块里就不用在再进行分组了*/
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn; //指定中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; //将抢占优先级设置为1,优先级是在多个中断源同时申请,产生拥挤的时候才有作用
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //将响应优先级设置为1
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_Init(&NVIC_InitStruct);
}
/*中断函数:中断函数的名字是固定的,在"startup_stm32f10x_md.s"文件里查看*/
/**
*函数:按键开始或播放音乐
*参数:无
*返回值:无
**/
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line7)==SET)
{
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == 0) //读PA7输入寄存器的状态,如果为0,则代表按键1按下
{
printf("MusicState原始状态:%d\r\n",MusicState);
if(MusicState)
{
// Delay_ms(50);
Stop_Music(); //如果本来就在播放音乐,按键按下那就停止播放音乐,并将音乐状态标志清零
MusicState = 0;
printf("如果原来大于1,MusicState按下暂停后归0:%d\r\n",MusicState);
}
else if(MusicState == 0)
{
B_Music();
printf("MusicState播放音乐后:%d\r\n",MusicState);
}
}
EXTI_ClearITPendingBit(EXTI_Line7); //将通道7中断标志位清除
}
}
这里我们使用了串口1打印了一下音乐播放状态标志,用来确定一下按键无法正常工作的原因。
Key3.h
#ifndef __KEY_H
#define __KEY_H
void Key3_Init(void);
void Key_StopMusic(void);
#endif
通过以上函数,我们实现了按键控制蜂鸣器播放音乐或暂停,但是是有很大缺陷的代码,只能实现按键功能一次性使用,什么意思呢?如果我的音乐上电后初始状态为播放状态,我能够实现暂停以后再开启,然后需要等整首音乐播放完才能通过按键重启,并不能中途暂停;如果我的音乐初始状态非播放,我能通过按键控制开始播放音乐,然后需要等整首音乐播放完才能通过按键重启,并不能中途暂停。这是因为代码是从上往下执行的,而我们用来记录音乐播放状态的变量没办法再音乐播放过程中修改,所以用这种方法没办法实现顺利实现我们想要的功能。我们使用中断进入了一个时间很长的中断程序进入了音乐播放状态,如果能够生效,就相当于我在A中断运行的时候再去触发A中断,这显示是无法实现的。当然,使用定时器定时中断的功能或许能够实现,但是太过复杂,这里我们先不管,后面用FreeRtos解决这个问题。
3.4 合并LED+直流电机+蜂鸣器功能
LED和直流电机、蜂鸣器我们都使用了按键进行控制,我们将它们的按键驱动程序合并到一块。
这时候就会出现一个问题,当我使用中断进入音乐播放功能在工作时,我按下LED和直流电机的按键会发现无效,这是因为中断优先级的问题,因为使用中断进入音乐播放本来就在中断中,我们必须使用比音乐播放更高优先级的中断才能跳出音乐播放。进行如下修改:
Key.c
/**
*Encoding:GB2312
*文件功能:按键功能实现
**/
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Key.h"
#include "LED.h"
#include "Motor.h"
#include "stdbool.h"
#include "Buzzer.h"
#include "Music.h"
#include "Motor.h"
#include "stdio.h"
#include "usart1.h"
extern bool MotorState;
extern uint8_t MusicState;
/**
*函数:LED按键1初始化函数
*参数:无
*返回值:无
**/
void Key1_Init(void)
{
/*第1步:打开时钟外设*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); //开启GPIOC的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO的时钟
/*EXTI和NVIC的时钟一直是打开的,不需要开启:
EXTI作为一个独立外设,按理说应该是需要开启时钟的但是寄存器里没有EXTI时钟的控制位
NVIC是内核的外设,内核的外设是不需要开启时钟的
RCC管的都是内核外的外设*/
/*第2步:配置GPIO*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure); //将PC15引脚初始化为上拉输入
/*第3步:配置AFIO:
它的库函数是和GPIO在一个文件里的*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource15);
/*第4步:配置EXTI*/
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line15; //指定中断线为第15个线路
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //指定中断线的模式为中断触发
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //指定中断触发方式为上升沿和下降沿均触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE; //指定选择的中断线的新状态
EXTI_Init(&EXTI_InitStruct);
/*第5步:配置NVIC
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,在misc.h文件里*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/*设置优先级分组方式,需要注意的是,这个分组方式整个芯片只能用一种,所以这个分组代码整个工程只需要
执行依次就行了,如果把它放在模块里面进行分组,要确保每个模块分组都是选的同一个,也可以把这个分组代码
放在主函数的最开始,这样模块里就不用在再进行分组了*/
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn; //指定中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //将抢占优先级设置为1,优先级是在多个中断源同时申请,产生拥挤的时候才有作用
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //将响应优先级设置为2
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_Init(&NVIC_InitStruct);
}
/**
*函数:风扇按键2初始化函数
*参数:无
*返回值:无
**/
void Key2_Init(void)
{
/*第1步:打开时钟外设*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO的时钟
/*EXTI和NVIC的时钟一直是打开的,不需要开启:
EXTI作为一个独立外设,按理说应该是需要开启时钟的但是寄存器里没有EXTI时钟的控制位
NVIC是内核的外设,内核的外设是不需要开启时钟的
RCC管的都是内核外的外设*/
/*第2步:配置GPIO*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB1引脚初始化为上拉输入
/*第3步:配置AFIO:
它的库函数是和GPIO在一个文件里的*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
/*第4步:配置EXTI*/
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line1; //指定中断线为第1个线路
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //指定中断线的模式为中断触发
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //指定中断触发方式为上升沿和下降沿均触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE; //指定选择的中断线的新状态
EXTI_Init(&EXTI_InitStruct);
/*第5步:配置NVIC
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,在misc.h文件里*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/*设置优先级分组方式,需要注意的是,这个分组方式整个芯片只能用一种,所以这个分组代码整个工程只需要
执行依次就行了,如果把它放在模块里面进行分组,要确保每个模块分组都是选的同一个,也可以把这个分组代码
放在主函数的最开始,这样模块里就不用在再进行分组了*/
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI1_IRQn; //指定中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //将抢占优先级设置为2,优先级是在多个中断源同时申请,产生拥挤的时候才有作用
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
//将响应优先级设置为1
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_Init(&NVIC_InitStruct);
}
/**
*函数:蜂鸣器按键3初始化函数
*参数:无
*返回值:无
**/
void Key3_Init(void)
{
/*第1步:打开时钟外设*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO的时钟
/*EXTI和NVIC的时钟一直是打开的,不需要开启:
EXTI作为一个独立外设,按理说应该是需要开启时钟的但是寄存器里没有EXTI时钟的控制位
NVIC是内核的外设,内核的外设是不需要开启时钟的
RCC管的都是内核外的外设*/
/*第2步:配置GPIO*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0引脚初始化为上拉输入
/*第3步:配置AFIO:
它的库函数是和GPIO在一个文件里的*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource7);
/*第4步:配置EXTI*/
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line7; //指定中断线为第0个线路
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //指定中断线的模式为中断触发
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //指定中断触发方式为上升沿和下降沿均触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE; //指定选择的中断线的新状态
EXTI_Init(&EXTI_InitStruct);
/*第5步:配置NVIC
因为NVIC是内核外设,所以它的库函数是被ST发配到杂项这里来了,在misc.h文件里*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/*设置优先级分组方式,需要注意的是,这个分组方式整个芯片只能用一种,所以这个分组代码整个工程只需要
执行依次就行了,如果把它放在模块里面进行分组,要确保每个模块分组都是选的同一个,也可以把这个分组代码
放在主函数的最开始,这样模块里就不用在再进行分组了*/
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn; //指定中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; //将抢占优先级设置为1,优先级是在多个中断源同时申请,产生拥挤的时候才有作用
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //将响应优先级设置为1
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_Init(&NVIC_InitStruct);
}
/*中断函数:中断函数的名字是固定的,在"startup_stm32f10x_md.s"文件里查看*/
/**
*函数:按键翻转LED状态和音乐播放状态,因为LED和音乐播放共用了同一个中断函数
*参数:无
*返回值:无
**/
void EXTI15_10_IRQHandler(void) //中断函数的名字都是固定的,并且无参无返回值
{
if(EXTI_GetITStatus(EXTI_Line15)==SET)
{
if (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_15) == 0) //读PC15输入寄存器的状态,如果为0,则代表按键1按下
{
LED_Turn(); //LED翻转
}
EXTI_ClearITPendingBit(EXTI_Line15); //将通道15中断标志位清除
}
}
/*中断函数:中断函数的名字是固定的,在"startup_stm32f10x_md.s"文件里查看*/
/**
*函数:按键翻转风扇状态
*参数:无
*返回值:无
**/
void EXTI1_IRQHandler(void)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //读PB1输入寄存器的状态,如果为0,则代表按键2按下
{
if(MotorState)
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
//按键按下后,如果原来是转的,那么设置PWM为0,电机停止转动
MotorState = false;
}
else
{
Motor_SetSpeed(60); //按下按键后,如果电机原来不转,设置转速默认为60,电机开始转动
}
}
EXTI_ClearITPendingBit(EXTI_Line1); //将通道1中断标志位清除
}
/*中断函数:中断函数的名字是固定的,在"startup_stm32f10x_md.s"文件里查看*/
/**
*函数:按键开始或播放音乐
*参数:无
*返回值:无
**/
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line7)==SET)
{
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == 0) //读PA7输入寄存器的状态,如果为0,则代表按键1按下
{
printf("MusicState原始状态:%d\r\n",MusicState);
if(MusicState)
{
Stop_Music(); //如果本来就在播放音乐,按键按下那就停止播放音乐,并将音乐状态标志清零
MusicState = 0;
printf("如果原来大于1,MusicState按下暂停后归0:%d\r\n",MusicState);
}
else if(MusicState == 0)
{
B_Music();
printf("MusicState播放音乐后:%d\r\n",MusicState);
}
}
EXTI_ClearITPendingBit(EXTI_Line7); //将通道7中断标志位清除
}
}
Key.h
#ifndef __KEY_H
#define __KEY_H
void Key1_Init(void);
void Key2_Init(void);
void Key3_Init(void);
#endif
修改后,在main.c 中初始化上述几节程序并下载到STM32后,可以发现在播放音乐的同时能够控制LED和直流电机,但是就是音乐播放功能无法达到我们想要的效果。
3.5 DHT11温湿度传感器驱动
DHT11.c
/**
*Encoding:GB2312
*文件功能:温湿度传感器功能实现
**/
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "stdio.h"
#include "DHT11.h"
uint8_t DHT11_Result[5] = {0};
/**
*函数:DHT11初始化函数
*参数:无
*返回值:无
**/
void DHT11_Init(void)
{
/*打开时钟外设*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
/*配置GPIO*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_8); //DHT11采用单总线协议进行数据传输,DHT11空闲状态置为高电平
}
/**
*函数:DHT11初始化函数
*参数:无
*返回值:无
*注释:总线空闲状态应为高电平,主机把总线拉低等待DHT11响应,且拉低时间必须大于18ms
**/
void DHT11_Start(void)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_8);
Delay_ms(20); //主机拉低电平至少18ms发出开始信号
GPIO_SetBits(GPIOA,GPIO_Pin_8);
Delay_us(20); //主机拉高电平延时20~40us等待DHT11发出响应信号
}
/**
*函数:DHT11响应函数
*参数:无
*返回值:无
*注释:DHT11接收到主机开始信号后进行响应
**/
uint8_t DHT11_Response(void)
{
uint16_t time = 0;
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8)&& time < 100) // 开始信号发出后处于高电平状态,接收信号后主机应该收到低电平,但是可能不会立即切换,因此用循环消耗一下这个切换时间
{
Delay_us(1);
time++;
}
if (time >= 100)
return 1; // 返回1说明响应超时,可能电路出现问题
time = 0;
while(!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8)&& time < 100) // 真正的DHT11响应信号,80us低电平信号
{
Delay_us(1);
time++;
}
if (time >= 100)
return 1; // 返回1说明响应超时,可能电路出现问题
return 0;
}
/**
*函数:接收DHT11数据函数
*参数:无
*返回值:无
**/
uint8_t DHT11_Read_Bit(void)
{
uint16_t time = 0;
while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8)&& time < 100) //DHT11响应信号发出结束后,先输出80us高电平,准备发送数据
{
Delay_us(1);
time++;
}
if (time >= 100)
return 2; //返回2说明响应超时,可能电路出现问题
time = 0;
while (!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8) && time < 100) //DHT11发出50us低电平信号后发送传感器测量数据
{
Delay_us(1);
time++;
}
if (time >= 30) //返回2说明响应超时,可能电路出现问题
return 2;
Delay_us(30); //延时30us
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_8) == 0)
return 0;
else // 如果30us后还是高电平,说明DHT11发送的是1
return 1;
}
/**
*函数:接收DHT11字节数据函数
*参数:无
*返回值:无
**/
uint8_t DHT11_Read_Byte(void)
{
uint8_t data = 0;
uint8_t i = 0;
for (i = 0; i < 8; i++)
{
data <<= 1;
data = data | DHT11_Read_Bit();
}
return data;
}
/**
*函数:将接收到的DHT11数据转换为温湿度
*参数:无
*返回值:无
**/
void DHT11_Read_Data(uint8_t *pData)
{
DHT11_Start();
if (DHT11_Response())
return;
uint8_t i;
for (i = 0; i < 5; i++)
{
pData[i] = DHT11_Read_Byte();
}
if (pData[4] != pData[0] + pData[1] + pData[2] + pData[3])
{
for (i = 0; i < 5; i++)
{
pData[i] = 0;
}
}
}
/**
*函数:变换温湿度数据为常规数据
*参数:接收温湿度数据的结构体
*返回值:温湿度数据结构体
**/
dht11_result DHT11_GetResult(dht11_result *result)
{
DHT11_Read_Data(DHT11_Result);
result->humi = DHT11_Result[0]+DHT11_Result[1]/10.0;
result->temp = DHT11_Result[2]+DHT11_Result[3]/10.0;
return *result;
}
DHT11.h
#ifndef __DHT11_H
#define __DHT11_H
typedef struct{
float humi;
float temp;
}dht11_result;
void DHT11_Init(void);
void DHT11_Read_Data(uint8_t *pData);
dht11_result DHT11_GetResult(dht11_result *result);
#endif
3.6 OLED驱动
江科大OLED显示屏教程
我们直接移植江科大写好的驱动即可,我做了下改动,把显示正负号那里改成了空格,效果如下:
main.c
/**
*Encoding:GB2312
*文件功能:项目功能实现
**/
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "String.h"
#include "Delay.h"
#include "usart1.h" //这个串口是给电脑使用的,用来在串口助手显示信息
#include "usart2.h" //这个串口是给ESP8266使用的
#include "timer.h"
#include "Key.h"
#include "LED.h"
#include "OLED.h"
#include "Buzzer.h"
#include "Music.h"
#include "Motor.h"
#include "DHT11.h"
//#include "BH1750.h"
//#include "ESP8266.h"
int main(void)
{
// Key1_Init(); //按键1初始化,用于控制LED灯
// Key2_Init(); //按键2初始化,用来控制直流电机,模拟风扇
// Key3_Init(); //按键3初始化,用来控制蜂鸣器播放音乐
// LED_Init(); //LED_Init初始化
// Motor_Init(); //直流电机初始化
// B_Music(); //音乐播放功能初始化,初始化后直接开始播放音乐
Serial_Init(); //串口1初始化,用于在串口显示数据
DHT11_Init(); //DHT11温湿度传感器初始化
OLED_Init(); //OLED初始化函数
// bh1750_init();
// USART2_Init(115200); //串口2初始化,波特率为115200,用于ESP8266与STM32交互
// WIFI_GPIO_Init();
// Rst_WIFI();
// WIFI_Init();
// TIM3_Int_Init(9999,35999);
OLED_ShowChinese(0,0,"温度:");
OLED_ShowChinese(0,17,"湿度:");
OLED_ShowChinese(0,33,"亮度:");
OLED_ShowChinese(0,49,"音乐:停止播放");
OLED_Update();
while (1)
{
dht11_result MyDHT11Result;
MyDHT11Result= DHT11_GetResult(&MyDHT11Result);
printf("温度:%.1f;湿度:%.1f\r\n",MyDHT11Result.temp,MyDHT11Result.humi);
OLED_ShowFloatNum(49, 0, MyDHT11Result.temp, 2, 1, OLED_8X16);
OLED_ShowChinese(92,0,"℃");
OLED_ShowFloatNum(49, 17, MyDHT11Result.humi, 2, 1, OLED_8X16);
OLED_ShowString(92,17, "%RH", OLED_8X16);
OLED_Update();
}
}
3.7 BH1750光照传感器
BH1750驱动参考
BH1750使用I2C传输数据,我移植了上述博主的代码。不过在我这里好像有问题,感觉结果不是很准确,先不管了,我先把系统跑起来再说。
MyI2C.c
/**************************************************************************
* 文件名 :MyI2C.c
* 描述 :软件模拟IIC程序
****************************************************************************/
#include "stm32f10x.h" // Device header
#include "MyI2C.h"
#include "Delay.h"
/*引脚配置层*/
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(I2C_GPIO_PORT, I2C_SCL_GPIO_PIN, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C写SDA引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(I2C_GPIO_PORT, I2C_SDA_GPIO_PIN, (BitAction)BitValue); //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(I2C_GPIO_PORT, I2C_SDA_GPIO_PIN); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
/**
* 函 数:I2C初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
*/
void MyI2C_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(I2C_GPIO_CLK, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = I2C_SCL_GPIO_PIN | I2C_SDA_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_GPIO_PIN | I2C_SDA_GPIO_PIN); //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
/*协议层*/
/**
* 函 数:SDA引脚输入输出模式配置
* 参 数:mode,定义SDA引脚模式,大于0为输出模式,否则为输入模式
* 返 回 值:无
*/
void Set_I2C_SDAMode(uint8_t mode)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(mode > 0)
{// 设置为输出模式
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏或推挽,外部需要接上拉电阻
}
else
{// 设置为输入模式
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 浮空或上拉,外部需要接上拉电阻
}
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = I2C_SDA_GPIO_PIN;
GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);
}
/**
* 函 数:I2C起始
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); //释放SDA,确保SDA为高电平
MyI2C_W_SCL(1); //释放SCL,确保SCL为高电平
Delay_us(5);
MyI2C_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
MyI2C_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
Delay_us(5);
}
/**
* 函 数:I2C终止
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Stop(void)
{
MyI2C_W_SCL(0);
MyI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
Delay_us(5);
MyI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
Delay_us(5);
}
/**
* 函 数:I2C发送应答信号
* 参 数:ack,主机应答信号,1或0,1表示下一个时序不需要再接收数据,0表示下一个时序继续接收数据
* 返 回 值:返回1,表示程序运行正常
*/
uint8_t MyI2C_SendAck(int ack)
{
Set_I2C_SDAMode(1);
if(ack == 1)
{// 发送ACk
MyI2C_W_SDA(1);
}
else if(ack == 0)
{
// 发送ACk
MyI2C_W_SDA(0);
}
else
{
return 0; // 入参有误,发送失败
}
MyI2C_W_SCL(1);
Delay_us(5);
MyI2C_W_SCL(0);
Delay_us(5);
return 1; //发送成功返回1为真
}
/**
* 函 数:I2C接收应答信号
* 参 数:无
* 返 回 值:返回1,表示程序运行正常
*/
uint8_t MyI2C_ReciveAck(void)
{
uint8_t ack = 0;
uint8_t timeout = 5;
MyI2C_W_SDA(1);
Set_I2C_SDAMode(0); // SDA输入模式
MyI2C_W_SCL(1);
Delay_us(5);
while(timeout--)
{// 等待从设备发送ACK
if(MyI2C_R_SDA() == 0)
{// 读到应答信号
ack = 1;
}
else
{// 没有读到应答信号,继续等待
Delay_us(1);
if(timeout == 0)
{// 等待超时
ack = 0;
}
}
}
MyI2C_W_SCL(0);
Delay_us(5);
Set_I2C_SDAMode(1); // SDA输出模式
return ack;
}
/**
* 函 数:I2C发送一个字节
* 参 数:Byte 要发送的一个字节数据,范围:0x00~0xFF
* 返 回 值:无
*/
uint8_t MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,主机依次发送数据的每一位
{
if(0x80 & Byte)
{
MyI2C_W_SDA(1);
}
else
{
MyI2C_W_SDA(0);
}
Byte <<= 1;
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
Delay_us(5);
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
Delay_us(5);
}
return MyI2C_ReciveAck();
}
/**
* 函 数:I2C接收一个字节
* 参 数:无
* 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0,bit; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
Set_I2C_SDAMode(0); //SDA输入模式
for (i = 0; i < 8; i ++) //循环8次,主机依次接收数据的每一位
{
Byte <<= 1;
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
Delay_us(5);
if (MyI2C_R_SDA() == 1)
{
bit = 0x80;
}
else
{
bit = 0x00;
}
Byte |= bit;
MyI2C_W_SCL(0); //拉低SCL,从机在SCL低电平期间写入SDA
Delay_us(5);
}
Set_I2C_SDAMode(1); //SDA输出模式
return Byte; //返回接收到的一个字节数据
}
/**
* 函 数:I2C发送一个字节
* 参 数:addr:发送设备地址,
* 返 回 值:返回应答信号,返回1说明发送成功,返回0发送失败
*/
uint8_t MyI2C_SendBytes(uint8_t addr, uint8_t *buf, uint8_t buf_size)
{
uint8_t i;
uint8_t result = 0;
MyI2C_Start();
if(MyI2C_SendByte(addr << 1)) // 发送设备地址(7bit地址)
{// 收到应答,发送成功
for (i = 0; i < buf_size; i++) // 发送数据
{
if(MyI2C_SendByte(buf[i]))
{// 收到应答,发送成功
result = 1;
}
else
{// 没有收到应答,发送失败
result = 0;
}
}
}
MyI2C_Stop(); // 发送停止信号
return result;
}
/* IIC接收多个数据 */
uint8_t MyI2C_ReceiveBytes(uint8_t addr, uint8_t *buf, uint8_t buf_size)
{
uint8_t i;
uint8_t result = 0;
MyI2C_Start();
if(MyI2C_SendByte((addr << 1) | 1)) // 发送设备地址(7bit地址)
{
for (i = 0; i < buf_size; i++) // 连续读取数据
{
*buf++ = MyI2C_ReceiveByte();
if (i == buf_size - 1)
{
MyI2C_SendAck(1); // 最后一个数据需要回NACK
}
else
{
MyI2C_SendAck(0); // 发送ACK
}
}
result = 1;
}
MyI2C_Stop(); // 发送停止信号
return result;
}
MyI2C.h
#ifndef __MYI2C_H
#define __MYI2C_H
/*这里将软件I2C使用引脚及外设定义在.H文件里,方便以后更换外设时修改,移植时只需要对这四个定义修改即可*/
#define I2C_GPIO_CLK RCC_APB2Periph_GPIOB
#define I2C_GPIO_PORT GPIOB
#define I2C_SCL_GPIO_PIN GPIO_Pin_13
#define I2C_SDA_GPIO_PIN GPIO_Pin_14
void MyI2C_W_SCL(uint8_t BitValue);
void MyI2C_W_SDA(uint8_t BitValue);
uint8_t MyI2C_R_SDA(void);
void MyI2C_Init(void);
void Set_I2C_SDAMode(uint8_t mode);
void MyI2C_Start(void);
void MyI2C_Stop(void);
uint8_t MyI2C_SendAck(int ack);
uint8_t MyI2C_ReciveAck(void);
uint8_t MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
uint8_t MyI2C_SendBytes(uint8_t addr, uint8_t *buf, uint8_t buf_size);
uint8_t MyI2C_ReceiveBytes(uint8_t addr, uint8_t *buf, uint8_t buf_size);
#endif
BH1750.c
/**************************************************************************
* 文件名 :bh1750.c
* 描述 :光强传感模块
****************************************************************************/
#include "stm32f10x.h" // Device header
#include "bh1750.h"
#include "Delay.h"
#include "MyI2C.h"
#define LOG_ENABLE
#ifdef LOG_ENABLE
#include "stdio.h"
#define LOG printf
#else
#define LOG(format, ...)
#endif
uint8_t current_mode; // BH1750的测量模式
float current_light; // BH1750的测量光照值
// BH1750延时函数
void bh1750_Delay(uint16_t ms)
{// ms级延时,BH1750每次测量都需要时间,该函数用于等待测量结果
Delay_ms(ms);
}
// 写命令
uint8_t bh1750_write_cmd(uint8_t cmd)
{
return MyI2C_SendBytes(BH1750_ADDRESS_LOW, &cmd, 1);
}
// 写寄存器
uint8_t bh1750_read_regs(uint8_t *buf, uint8_t buf_size)
{
return MyI2C_ReceiveBytes(BH1750_ADDRESS_LOW, buf, buf_size);
}
// 复位
uint8_t bh1750_reset(void)
{
return bh1750_write_cmd(BH1750_RESET);
}
// 打开电源
uint8_t bh1750_power_on(void)
{
return bh1750_write_cmd(BH1750_POWER_ON);
}
// 关闭电源
uint8_t bh1750_power_down(void)
{
return bh1750_write_cmd(BH1750_POWER_DOWN);
}
// 设置测量模式
uint8_t bh1750_set_measure_mode(uint8_t mode)
{
uint8_t result = 0;
if(bh1750_write_cmd(mode))
{
result = 1;
}
return result;
}
// 单次读取光照值
uint8_t bh1750_single_read_light(uint8_t mode, float *light)
{// 单次测量模式在测量后会自动设置为断电模式
uint8_t temp[2];
uint8_t result = 0;
if(mode != BH1750_ONE_H_RES_MODE && mode != BH1750_ONE_H_RES_MODE2 && mode != BH1750_ONE_L_RES_MODE)
{
LOG("bh1750 single read measure mode error! mode:0x%02x\r\n", mode);
return result;
}
if(bh1750_set_measure_mode(mode)) // 每次采集前先设置模式
{
LOG("bh1750 set measure mode success! mode:0x%02x\r\n", mode);
current_mode = mode;
switch (mode)
{
case BH1750_ONE_H_RES_MODE: // 单次H分辨率模式(精度1lx,测量时间120ms)
bh1750_Delay(120); // 等待采集完成
break;
case BH1750_ONE_H_RES_MODE2: // 单次H分辨率模式(精度0.5lx,测量时间120ms)
bh1750_Delay(120); // 等待采集完成
break;
case BH1750_ONE_L_RES_MODE: // 单次L分辨率模式(精度4lx,测量时间16ms)
bh1750_Delay(16); // 等待采集完成
break;
default:
break;
}
if(bh1750_read_regs(temp, 2)) // 读取测量结果
{
*light = ((float)((temp[0] << 8) + temp[1]) / 1.2); // 换算成光照值
result = 1;
}
else
{
LOG("bh1750 read light failed!\r\n");
}
}
else
{
LOG("bh1750 set measure mode failed! mode:0x%02x\r\n", mode);
return result;
}
return result;
}
// 连续读取光照值
uint8_t bh1750_continuous_read_light(uint8_t mode, float *light)
{
uint8_t temp[2];
uint8_t result = 0;
if(mode != BH1750_CON_H_RES_MODE && mode != BH1750_CON_H_RES_MODE2 && mode != BH1750_CON_L_RES_MODE)
{
LOG("bh1750 continuous read measure mode error! mode:0x%02x\r\n", mode);
return result;
}
if(mode != current_mode)
{// 要使用的测量模式和BH1750当前的模式不同,则配置成相同模式
if(bh1750_set_measure_mode(mode))
{
LOG("bh1750 set measure mode success! mode:0x%02x\r\n", mode);
current_mode = mode;
}
else
{// 模式设置失败
LOG("bh1750 set measure mode failed! mode:0x%02x\r\n", mode);
return result;
}
switch (mode)
{
case BH1750_CON_H_RES_MODE: // 连续H分辨率模式(精度1lx,测量时间120ms)
bh1750_Delay(120); // 等待采集完成
break;
case BH1750_CON_H_RES_MODE2: // 连续H分辨率模式(精度0.5lx,测量时间120ms)
bh1750_Delay(120); // 等待采集完成
break;
case BH1750_CON_L_RES_MODE: // 连续L分辨率模式(精度4lx,测量时间16ms)
bh1750_Delay(16); // 等待采集完成
break;
default:
break;
}
}
if(bh1750_read_regs(temp, 2)) // 读取测量结果
{
*light = ((float)((temp[0] << 8) + temp[1]) / 1.2); // 换算成光照值
result = 1;
}
else
{
LOG("bh1750 read light failed!\r\n");
}
return result;
}
// BH1750初始化
uint8_t bh1750_init(void)
{
uint8_t result = 0;
MyI2C_Init(); // IIC初始化
result = bh1750_power_on(); // 打开BH1750电源
current_mode = 0;
return result;
}
// 单次读取BH1750例程
void bh1750_read_example(void)
{
bh1750_single_read_light(BH1750_ONE_H_RES_MODE2, ¤t_light);
printf("光照强度为:%0.1f\r\n",current_light);
Delay_us(100);
}
BH1750.h
#ifndef __BH1750_H__
#define __BH1750_H__
#include "stm32f10x.h"
#define BH1750_ADDRESS_LOW 0x23 // addr low 7bit:0x23 8bit:0x46
#define BH1750_ADDRESS_HIGH 0x5C // addr high 7bit:0x5C 8bit:0xB8
/*bh1750 registers define */
#define BH1750_POWER_ON 0x01 // power on
#define BH1750_POWER_DOWN 0x00 // power down
#define BH1750_RESET 0x07 // reset
#define BH1750_CON_H_RES_MODE 0x10 // Continuously H-Resolution Mode
#define BH1750_CON_H_RES_MODE2 0x11 // Continuously H-Resolution Mode2
#define BH1750_CON_L_RES_MODE 0x13 // Continuously L-Resolution Mode
#define BH1750_ONE_H_RES_MODE 0x20 // One Time H-Resolution Mode
#define BH1750_ONE_H_RES_MODE2 0x21 // One Time H-Resolution Mode2
#define BH1750_ONE_L_RES_MODE 0x23 // One Time L-Resolution Mode
uint8_t bh1750_init(void);
void bh1750_read_example(void);
#endif
04 STM32F103移植FreeRTOS
上面我们完成所有外设驱动程序得编写,那么为了解决上面提到的问题,现在我们进行移植FreeRTOS了。
参考教程:
FreeRTOS入门与工程实践-韦东山老师
FreeRTOS实践教程
FreeRTOS书籍-正点原子
上述资料先看韦东山老师的课程,韦东山老师讲解了一些必要的理论知识,有助于第二个实践课程的理解;建议直接刷完两套视频,刷视频过程中理解即可,不用跟着做,然后对着书自己做FreeRTOS项目。
这个版本的智能家居项目仅仅用到了FreeRTOS的任务创建、挂起与恢复以及中断,还有很多需要改进的地方,缺点也比较明显,后期看情况进行迭代。
05 Qt上位机开发
参考:
Qt仪表盘开发
Qt串口助手开发