使用STM32实现智能桌面风扇控制系统
目录
一、功能介绍
二、硬件清单
三、模块详解
1)电机驱动
2)舵机驱动
3)定时关闭
4)温度传感器
5)FLASH
6)按键控制
7 )多级菜单
8)主函数
四、源码可私
一、功能介绍
(1)输入电压为DC12-24V; 电源模块
(2)支持不同风扇挡位调节风速,每个挡位有对应的指示;最低3档,最高可设置5档;
(3)支持风扇定时操作,风扇定时的时间能够可视化;
(4)需支持风扇左右摆头,摆头角度不小于120°;
(5)风扇能够监测当前环境的温度并显示出来;
(6)风扇需支持支持断电记忆,在断电再次上电后,还能够记忆断电前的挡位以及模式。
(7)支持智能模式以及手动模式切换选择:
a)智能模式:开启智能模式后,风扇能够自行检测当前环境的温度来调节不同的挡位;
b)手动模式:开启手动模式后,风扇的转速和摇头全部转接到按键控制,脱离智能温度调节;
二、硬件清单
主控芯片 | STM32F103C8T6 |
温度传感器 | DS18b20 |
电机驱动 |
L9110 |
电源 | LM259 |
OLED | 七针 |
FLASH | W25Q64 |
舵机 | 180° |
三、模块详解
1)电机驱动
L9110是为控制和驱动电机设计的两通道推挽式功率放大专用集成电路器件,将分立电路集成在单片IC之中,使外围器件成本降低,整机可靠性提高。该芯片有两个TTL/CMOS兼容电平的输入,具有良好的抗干扰性;两个输出端能直接驱动电机的正反向运动,它具有较大的电流驱动能力,每通道能通过750~800mA的持续电流,峰值电流能力可达1.5~2.0A;同时它具有较低的输出饱和压降;内置的钳位二极管能释放感性负载的反向冲击电流,使它在驱动继电器、直流电机、步进电机或开关功率管的使用上安全可靠。L9110被广泛应用于玩具汽车电机驱动、步进电机驱动和开关功率管等电路上。
使用PWM引脚来控制电机速度,不同的PWM对应不同的风速
2)舵机驱动
控制原理:
通过向舵机的信号信号线发送PWM信号来控制舵机的输出量;
一般来说,PWM的周期以及占空比,我们是可控的,所以PWM脉冲的占空比直接决定了输出轴的位置。
舵机的控制一般需要一个20ms左右的时基脉冲,该脉冲的高电平部分一般为0.5ms-2.5ms范围内的角度控制脉冲部分,总间隔为2ms。以180度角度为例,其控制关系是这样的:
0.5ms————–0度;
1.0ms————45度;
1.5ms————90度;
2.0ms———–135度;
2.5ms———–180度;
代码中,ARR=1999,PSC=719,由
定时频率=72M/ (PSC + 1) / (ARR + 1)
知,T=20ms。
设置占空比:TIM_SetCompare2(TIM2, Compare)
比例关系
若为0.5ms,则compare=50,即为0°;
若为2.5ms,则compare=250,即为180°
Reference:(36条消息) STM32f1之舵机驱动+转动角度调整(含主代码和计算)_舵机角度如何调整_不说二话的自家人的博客-CSDN博客
初始化代码:(电机与舵机共用一个PWM)
void PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 |GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 2000 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC(20ms——对应舵机)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
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 = CCR1_Val; //CCR(输出口1)
TIM_OC3Init(TIM2, &TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = CCR2_Val; //CCR(输出口2)
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); //使能SWD 禁用JTAG
TIM_Cmd(TIM2, ENABLE);
}
3)定时关闭
设计思路:
开启定时后,使能定时器,每1s产生一次中断。中断函数中,计数秒数并进行判断,到达相应的秒数后,关停风扇,定时器失能。
初始化函数:
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
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=10000-1;//周期;ARR自动重装器的值(1s计数一万次)
TIM_TimeBaseInitStructure.TIM_Prescaler=7200-1;//PSC预分频器的值(arr和psc是配置计数间隔的)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;//重复计数器的值(高级有)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);//配置时基单元
TIM_ClearFlag(TIM3,TIM_FLAG_Update);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
}
中断函数:
void TIM3_IRQHandler(void)//计时1s进入中断
{
if(TIM_GetFlagStatus(TIM3,TIM_IT_Update)==SET)//获取中断标志位
{
Num++;
if(Num==10) {timestop(1);}//改定时时间
if(Num==120){timestop(2);}
if(Num==7200) {timestop(3);}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}
}
//timestop函数中,关停风扇,即设风扇PWM为2000。定时器失能TIM_Cmd(TIM3,DISABLE)
4)温度传感器
输入输出的引脚初始化:(输出为推挽模式,输入为上拉模式)
void DS18B20_Output_Input(u8 cmd)
{
GPIO_InitTypeDef GPIO_InitStructure;
if(cmd)//out
{
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_10MHz;
}
else//input
{
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;//上拉模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
}
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
工作步骤
DS18B20的工作步骤可以分为三步:
1.初始化DS18B20
2.执行ROM指令
3.执行DS18B20功能指令
1.初始化DS18B20
首先进行在初始化序列期间,总线上的主设备通过拉低1-Wire总线超过480us来发送(TX)复位脉冲。之后主设备释放总线而进入接收模式(RX)。当总线释放后,5kΩ左右的上拉电阻将1-Wire总线拉至高电平。当DS18B20检测到该上升边沿信号后,其等待15us至60us后通过将1-Wire总线拉低60us至240us来实现发送一个存在脉冲。
2.执行ROM指令
访问每个DS18B20,搜索64位序列号,读取匹配的序列号值,然后匹配对应的DS18B20,如果我们仅仅使用单个DS18B20,可以直接跳过ROM指令。而跳过ROM指令的字节是0xCC。
3.写时序
在主设备初始化写时段后,DS18B20将会在15us至60us的时间窗口内对总线进行采样。如果总线在采样窗口期间是高电平,则逻辑1被写入DS18B20;若总线是低电平,则逻辑0被写入DS18B20。
void DS18B20_Write_Byte(u8 data)
{
for(u8 i=0;i<8;i++)
{
DS18B20_Output_Input(1);
GPIO_ResetBits(GPIOA,GPIO_Pin_0);//拉低2us
Delay_Us(2);//大于1us
(data&0x01) ? GPIO_SetBits(GPIOA,GPIO_Pin_0):GPIO_ResetBits(GPIOA,GPIO_Pin_0);//从低位开始读
Delay_Us(45);//写入时间
GPIO_SetBits(GPIOA,GPIO_Pin_0);//释放总线
data>>=1;//右移
}
}
注意:2次写周期之间至少间隔1us
4.读时序
读时隙由主机拉低总线电平至少1μs然后再释放总线,读取DS18B20发送过来的1或者0
DS18B20在检测到总线被拉低1us后,便开始送出数据,若是要送出0就把总线拉为低电平直到读周期结束。若要送出1则释放总线为高电平。
u8 DS18B20_Read_Byte(void)
{
u8 data=0;
for(u8 i=0;i<8;i++)
{
data>>=1;//和 写 接着(读操作是从右边位起,上一次无论是读或写都结束在最低位,右移一位回到最高位)
DS18B20_Output_Input(1);
GPIO_ResetBits(GPIOA,GPIO_Pin_0);//拉低2us
Delay_Us(2);
GPIO_SetBits(GPIOA,GPIO_Pin_0);//释放总线
DS18B20_Output_Input(0);
if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==SET)//1
{
data |=0x80;//从高位开始或,低位变成高位高位变成低位
}//0会自动补0
Delay_Us(45);
}
return data;
}
DS18B20_Startup();
DS18B20_Write_Byte(0XCC);//跳过ROM(只有一个温度传感器)
DS18B20_Write_Byte(0X44);//温度转换
Delay_Ms(750);//12位分辨率 750毫秒
DS18B20_Startup();//读取寄存器
DS18B20_Write_Byte(0XCC);
DS18B20_Write_Byte(0Xbe);//读ram
DS18B20采用16位补码的形式来存储温度数据,温度是摄氏度。当温度转换命令发布后,经转换所得的温度值以二字节补码形式存放在高速暂存存储器的第0和第1个字节。
高字节的五个S为符号位,温度为正值时S=1,温度为负值时S=0
剩下的11位为温度数据位,对于12位分辨率,所有位全部有效,对于11位分辨率,位0(bit0)无定义,对于10位分辨率,位0和位1无定义,对于9位分辨率,位0,位1,和位2无定义
LSB=DS18B20_Read_Byte();//先读低位
MSB=DS18B20_Read_Byte();
temp=(MSB<<8)|LSB;//合并字节
if((temp&0xF800)== 0xF800)
{
*data=((~temp+0x01)*-0.625)+0.5;//四舍五入小数点后一位
}
else
{
*data=(temp*0.0625)+0.5;
}
调用:DS18B20_Read_Temp(&tempe); //tempe即为返回的温度值
Reference:(36条消息) DS18B20主要指令_ds18b20命令_煤炭的奇妙漂流的博客-CSDN博客
(36条消息) 【常用传感器】DS18B20温度传感器原理详解及例程代码_Z小旋的博客-CSDN博客
5)FLASH
- 先建一个SPI模块,实现引脚封装、初始化以及SPI通信起始、终止和交换一个字符
输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入(对主机来说)
void MySPI_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_Pin_7;
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_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
MySPI_W_SS(1);//默认不选中从机
MySPI_W_SCK(0);//使用模式0
}
void MySPI_Start(void)//起始信号
{
MySPI_W_SS(0);
}
void MySPI_Stop(void)//终止信号
{
MySPI_W_SS(1);
}
发送数据(采用模式0)
SS置0→数据移出→SCK上升沿→数据读入→SCK下降沿
2.建W25Q64模块,调用SPI的拼图,来拼接各种指令和功能的完整时序,比如写使能,擦除,页编程,读数据等等
按照指令表格式编写代码
void W25Q64_WaitBusy(void)
{
uint32_t Timeout;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);//指令
Timeout = 100000;
while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
{
Timeout --;//避免直接写死循环使程序卡死
if (Timeout == 0)
{
break;
}
}
MySPI_Stop();
}
void W25Q64_PageProgram(uint32_t Address, uint16_t *DataArray, uint16_t Count)//页编程
{
uint16_t i;
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//页编程指令
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for (i = 0; i < Count; i ++)
{
MySPI_SwapByte(DataArray[i]);//写入多个字节
}
MySPI_Stop();
W25Q64_WaitBusy();//事后等待
}
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
MySPI_Stop();
W25Q64_WaitBusy();
}
void W25Q64_ReadData(uint32_t Address, uint16_t *DataArray, uint32_t Count)
{
uint32_t i;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_DATA);//发送指令
MySPI_SwapByte(Address >> 16);//写入高位地址
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for (i = 0; i < Count; i ++)
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
}
MySPI_Stop();
}
参考b站江协科技spi通信相关视频
6)按键控制
共有5个按键,作用分别是上翻、下翻、返回、确定、关停风扇。
初始化函数:
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11| GPIO_Pin_6| GPIO_Pin_7|GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
获取键值(同时消抖):
uint8_t Get_KEY_Value(void)
{
uint8_t KeyNum = 0;
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == 0)//按键按下
{
Delay_Ms(20);//消抖
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == 0);
Delay_Ms(20);
KeyNum = 2;
}
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)
{
Delay_Ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);
Delay_Ms(20);
KeyNum = 3;
}
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6) == 0)
{
Delay_Ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6) == 0);
Delay_Ms(20);
KeyNum = 4;
}
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) == 0)
{
Delay_Ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) == 0);
Delay_Ms(20);
KeyNum = 5;
}
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5) == 0)
{
Delay_Ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5) == 0);
Delay_Ms(20);
KeyNum = 6;
}
return KeyNum;
}
7 )多级菜单
使用数组查表来实现多级菜单。
设计框架:
定义结构体:
typedef struct
{
u8 Cur_Index;//当前索引项
u8 previous;//上一页
u8 next;//下一页
u8 enter;//确认
u8 back;//返回
void (*current_operation)(u8,u8);// 当前索引执行的函数(界面)
}Main_Menu;
定义索引表:
Main_Menu table[20]=
{
//Cur_Index,previous(上一个),next(下一个),enter,back,(*current_operation)(u8,u8)
//主界面
{_Main_UI,_Main_UI,_speed_set,_speed_set,_Main_UI,Main_UI},
//主菜单
{_speed_set,_mode_set,_time_set,_speed_child,_Main_UI,Main_Menu_Func},//风速调节
{_time_set,_speed_set,_tou_set,_time_child,_Main_UI,Main_Menu_Func},//定时操作
{_tou_set,_time_set,_mode_set,_tou_child,_Main_UI,Main_Menu_Func},//摆头设置
{_mode_set,_tou_set,_speed_set,_mode_child,_Main_UI,Main_Menu_Func},//模式设置
//子菜单
{_speed_child,_speed_child,_speed_child,_speed_child,_speed_set,speed_child},//风速调节子菜单
{_time_child,_time_child,_time_child,_time_child,_time_set,time_child},//定时操作子菜单
{_tou_child,_tou_child,_tou_child,_tou_child,_tou_set,tou_child},//摆头设置子菜单
{_mode_child,_mode_child,_mode_child,_mode_child,_mode_set,mode_child},//模式设置子菜单
};
比如说,我现在的界面(cur_Index)是_speed_set,那我按下上翻键,我就要跳到上一个索引值对应的界面(_mode_set), 按下翻键我就要跳到下一个对应的界面(_time_set)。实现的代码如下(不断获取按键值,当按键值有变化时,更新索引值,跳转界面)
void GUI_Refresh(void)
{
u8 key_val=Get_KEY_Value();
if(key_val!=0)//只有按键按下才刷屏
{
last_index=func_index;//更新上一界面索引值
switch(key_val)
{
case KEY_PREVIOUS: func_index=table[func_index].previous;//更新索引值
break;
case KEY_ENTER: func_index=table[func_index].enter;//更新索引值
break;
case KEY_NEXT:func_index=table[func_index].next;//更新索引值
break;
case KEY_BACK:func_index=table[func_index].back;//更新索引值
break;
case KEY_STOP:fsspeed =2000;Motor_SetSpeed();;//关风扇
break;
default:break;
}
OLED_Clear();//清屏
}
current_operation_func=table[func_index].current_operation;
(*current_operation_func)(func_index,key_val);//执行当前索引对应的函数
}
同时,在gui.c中,不同的界面的按键选择传回不同的控制参数,来使包括电机、舵机、温度传感器等外设实现工作状态的切换。
Reference:(36条消息) STM32简易多级菜单(数组查表法)_stm32多级菜单_码农爱学习的博客-CSDN博客
8)主函数
调用一系列初始化函数,读出flash中的内容,保持上次断电前的风速及模式。while(1)循环中,只做按键值的获取和oled屏的更新,如果再写其他带延时的程序,会导致按键值获取不灵敏,屏幕更新缓慢的问题。
int main(void)
{
OLED_Init();
Key_Init();
PWM_Init();
Motor_Init();
W25Q64_Init();
W25Q64_ReadData(0x000000, ArrayRead, 4);//读出
switch(ArrayRead[0])
{
case 0:fsspeed=2000;break;
case 1:fsspeed=1100;break;
case 2:fsspeed=500;break;
case 3:fsspeed=0;break;
default:break;
}
mode=ArrayRead[1];
Motor_SetSpeed();//先读出flash再初始化
DS18B20_Init();
Timer_Init();
modeTimer_Init();
OLED_Clear();//清屏
if(mode==1)
{
TIM_Cmd(TIM4,ENABLE);
}
while (1)
{
GUI_Refresh();
}
}
四、源码可私
代码文件架构参考:[2-2] 新建工程_哔哩哔哩_bilibili