Bootstrap

​第1个小项目:基于stm32的小夜灯实现

目录

前言

一、开发工具和部件

二、思路来源

三、内部工作逻辑的构思

四、具体实现过程    

总结


前言

  • 这个专栏系列旨在帮助刚入门的单片机萌新(尤其适用于听过基础知识想要自行拓展的萌新)解决我们自己着手开发时会遇到的诸多问题。我并不是大师,顶多算是个脑子里天马行空的梦想家。但正因为如此我所遇到的问题往往也是众多像我一样的萌新在捣鼓过程中会遇到的共性问题。我在这一过程中也查阅了许多资料,却鲜少有人能够一针见血地指出萌新常犯的一些错误。这些错误很微小,却能让人血压显著升高。我也是一个初学者,没有好为人师的毛病。我只是希望能以这个栏目向大家呈现一些我的小成果和思路吧。坦白地说,单片机的学习过程是枯燥且艰难的,因此我希望以一种诙谐的方式向大家呈现出我“犯傻”的过程。同时我也会适量减少专业术语的使用以降低理解难度我一直坚信,“曲高而和寡”不是一件好事,如果这个系列里的文章对你有所帮助,还请不要吝啬你手里的赞,这是对笔者最好的鼓励。有其他更好的想法或者发现了文章中出现的错误,也欢迎你的私信和指正。不喜勿喷!不喜勿喷!不喜勿喷!那么事不宜迟,我们马上开始!!

一、开发工具和部件

  • Keil5 MDK
  • stm32 F103C8T6最小系统板
  • 0.96寸4引脚OLED显示屏
  • 光敏电阻传感器模块,光敏电阻型号5516
  • LED灯
  • ST-Link烧写器
  • 公对母和母对母的杜邦线若干

二、思路来源

       在看完b站江科大的stm32 AD多通道数模信号转换的课程后,我意识到这是外界需要观测的物理量与单片机之间的桥梁。也就是说,通过外部模块和ADC数模转换器,我们成功实现了从物理量到模拟量再到数字量的转变这也为我们程序上的调控提供了理论可行性。当然呢,外界模块(这里是传感器)有多种多样的类型,我们可以根据自己的实际需要和所用单片机的型号进行选择,一般来说是通用的。这里我选择的是最容易量化的物理量:光强,也就是利用光敏电阻模块来实现数据的变化和采集。

三、内部工作逻辑的构思

  1. 根据ADC16位规则通道数据寄存器中的值,我们可以大概得知在弱光状态下,寄存器的值为1800左右。而在强光下,寄存器的值为2800左右。这里的强光与弱光的判定,标准是观察光敏电阻模块上自带的的DO LED的亮灭。对于光敏电阻来说,当外界的光强越大,光敏电阻的阻值越小。因此当外界是强光环境时,DO LED处于点亮状态。据此我们可以通过肉眼判定当寄存器中的值为2500左右为中介值,这样我们便确定了控制LED亮灭的寄存器阈值。当然,这个值是粗略的,但是并不影响我们代码的实现,毕竟也用不到那么高的精度。
  2. 关于如何对ADC内部寄存器的值进行有效的监视,可以通过模拟看门狗,也可以通过配置TIM定时器中断。这里的话模拟看门狗我还不是很熟悉,因此果断采用第二个方案。后面根据学习进度的调整我会再出一章plus版本用模拟看门狗来实现这一过程,继续向大家继续阐述一个纯萌新在这之中的遇到的困难,想法和一些解决办法。
  3. 二号计划的第三步是在OLED屏幕上同步显示LED灯目前的开关状态,在这里一号计划遇到了很大的困难,后面会详细阐述。
  4. 至于部件的连接,LED灯找一个正常的GPIO(通用输入输出)口,同时将长引脚(二极管的正极)接正极,短引脚接GPIO口的引脚这样当GPIO口置低电平时LED亮,反之则暗。这里我选择的是PA6引脚。
    void LED1_ON(void)
    {
    	GPIO_ResetBits(GPIOA, GPIO_Pin_6);//低电平
    }
    
    void LED1_OFF(void)
    {
    	GPIO_SetBits(GPIOA, GPIO_Pin_6);
    }
  5. 光敏电阻模块根据引脚定义图连接在一个引脚复用有诸如ADC12_IN1的引脚即可。注意VCC接正,GND接负,不要一不小心接反了,那就翻车了(不要问我怎么知道的,问就是经验)。这里我选择的是PA1引脚。

四、具体实现过程

     1.首先要在System文件夹下添加Timer.c和Timer.h,当然放其他文件夹也行,只要在Keil5里            配置好了文件夹路径即可(此处总是报错的同学可自行观看江科大的教程,在此不再赘述)

     2.萌新最重要的是不要在开发过程中丢三落四。像我的话在配置好模块之后忘了在main.c文件里引用头文件和初始化模块,结果总是报错。耐心一些,总归可以的。同时在写代码的过程中大家也要对格式问题引起重视。该空格的地方要空格。如果代码胡成一团编译器不会报错,但是这一方面降低了代码的可读性,另一方面也有可能造成意想不到的Bug。我之前在写PWM(脉冲宽度调制)调控SG90舵机和直流电机的时候就会出现代码与老师几乎一样,但是运行结果依托答辩的情况。如果你也遇到了这个问题,一个可以将之前文件夹中写的太多没什么用的模块代码删除,同时修正格式。还可以借用别人的健康代码,将代码参考过来后编译一次再删除代码自己写,往往问题得到解决。这个问题我之前也悬赏过,待后面研究得更深入后我会再分享出来。

     3.我的定时器初始化函数配置如下,上面都有详细的注释,方便大家回顾基础知识。必要的时候大家也要多翻一翻开发手册,自己绘制或者是看江科大老师的简化原理图都行,将这一路上的外设都配置完成,我们的工作便已成功了一半。

void Timer_Init()
{
	//开启RCC系统时钟,开启通用定时器TIM2
	//注意TIM2是APB1总线上的外设
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	//第二步 选择驱动时基单元的时钟
	//选择的是内部时钟,其余可以在Library文件夹内的tim.h内查找
	//也可以不写,上电后默认是内部时钟
    TIM_InternalClockConfig(TIM2);
	//初始化时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	//指定时钟分频(降频)
	//TIM_CKD_DIV1(不分频)、TIM_CKD_DIV2(2分频) 、TIM_CKD_DIV4(4分频)
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	//配制计数模式 向上计数、向下计数、三种中央对齐模式
	//这里选择向上计数
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	//周期,ARR自动重装器的值
	//下面给出计算定时的公式
	//CK_CNT_OV(定时频率/计数器溢出频率)=(CK_PSC(系统时钟频率72MHZ)/(PSC+1))/(ARR+1)
	//定时1s,也就是定时频率为1HZ
	//可以令PSC=7200-1 ARR=10000-1
	//PSC和ARR的取值不能超过0-65535(2^16-1),两个寄存器都是16位的
	//这里对TIM2进行了7200的分频,得到的是10KHZ的计数频率
	//在10KHZ的频率下计10K个数,需要花1s的时间
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
	//PSC预分频器的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	//重复计数器的值,高级定时器才有,这里直接给0
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
	
	//使能更新中断
	//TIM_IT_Update更新中断
	//手动将更新中断标志位清除,避免一上电就进入中断
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
	
	//NVIC的配置和初始化
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//2位响应 2位抢占
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	//指定中断通道是使能还是失能 ENABLE/DISABLE
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	
	//使定时器可以开始工作
	TIM_Cmd(TIM2, ENABLE);
}

      当然,这里我们只涉及到一个中断,关于中断优先级的问题可以不做过多考虑,至于其他的配        置也是一样。

     4.接下来如法炮制,我们来写一下数模转换器的模块吧。

void AD_Init(void)
{
	//打开RCC时钟,驱动ADC1数模转换器和GPIO口工作
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	//配置ADC预分频,此处是六分频,则ADC内部基准频率是12MHZ
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	//将GPIO1配置成模拟输入模式(ADC专属模式)
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//初始化ADC
	ADC_InitTypeDef ADC_InitStructure;
	//ADC的工作模式(独立模式/双ADC模式),此处选择的是独立模式
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	//这里选择数据右对齐,方便直接读出值
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	//外部触发转换选择(选择控制触发的触发源),这里选择软件触发 
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	//连续转换模式,此处选择的是单次转换
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	//扫描转换模式,此处选择的是非扫描模式
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;
	//指定在扫描模式下总共会用到的通道数
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	ADC_Init(ADC1, &ADC_InitStructure);
	//开启ADC
	ADC_Cmd(ADC1, ENABLE);
	
	//复位校准
	ADC_ResetCalibration(ADC1);
	//只有标志位清0,才会跳出这个循环
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	//开始校准状态
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

uint16_t AD_GetValue(uint8_t ADC_Channel)
{
	//可指定通道的规则组配置
	使用的数模转换器, 想指定的通道(菜单), 序列号(菜品顺序)(1-16), 指定通道的采样时间
	ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);
	//软件触发转换
	//若是连续转换,则可将55行转移到49行,同时去掉57行即可
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	//等待转换完成(等待EOC(转换结束位)标志位置1)   规则组转换标志位
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//大概会等待5.6us(见前面)
	//读取ADC数据寄存器
	return ADC_GetConversionValue(ADC1);
}

       写完的函数别忘了在.h文件里面声明,模块的.c文件里要先包含stm32的头文件,其他应该             就没什么了。

   5.接下来我们来写主函数。

#include "Delay.h"
#include "OLED.h"
#include "AD.h"
#include "Timer.h"
#include "LED.h"
uint16_t AD1;

int main(void)
{
	OLED_Init();
	AD_Init();
	LED_Init();
	Timer_Init();
	OLED_ShowString(1, 1, "light:");
	while (1)
	{
		AD1 = AD_GetValue(ADC_Channel_1);
		Delay_ms(50);
	}
}

首先还是别忘了引用头文件和使用模块初始化函数。刚开始我的计划是:我们让其实现小夜灯的功能,同时监测此时ADC寄存器的值,将其显示在OLED屏幕上。但是我很快发现了问题。程序会根据寄存器的值频繁进入中断,因为中断函数执行的是对环境的监测并做出对应指令的工作。这就导致OLED屏幕上寄存器的值处于“静止”状态。我尝试在中断函数里加了一个while循环,同时设定若过1000ms寄存器的值稳定在一个范围之内就跳出循环并执行中断标志位的清除,但是没有效果。原因还是上面的那个。这我可就翻车了呀。思来想去,算了,我改为在OLED屏上输出LED灯的状态吧。但是又出现了问题:为什么ON后面还有个F?原来是我的中断函数执行的时候由于OFF比ON了个字母,刷新的时候没有刷新掉导致的,将原本的"ON"改为"ON "后就好了。下面是中断函数的代码。

void TIM2_IRQHandler(void)
{
		if(AD_GetValue(ADC_Channel_1) > 2500)
		{
			Delay_ms(100);
			LED1_ON();
			OLED_ShowString(1, 8, "ON ");
		}
		else
		{
			Delay_ms(100);
			LED1_OFF();
			OLED_ShowString(1, 8, "OFF");
			
		}
	
	TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}

那么我们来看一下效果吧!


总结

       这是一个非常简单的程序自动化调控的实现。但是简单不代表就好做,我前前后后忙了一个小时才摸索出来(我是菜狗)。总而言之,这便是我的第一个作品啦!码字不易,感谢读到这里的你!
;