谨以此文和我去年前的一篇蓝桥杯单片机的教程构成电子类的青铜双壁.
国信长天单片机竞赛训练之原理图讲解及常用外设原理(遗失的章节-零)_昊月光华的博客-CSDN博客
目录
时钟树
串口重定向:printf输出
动态点灯(点灯大师)
按键(常用状态机)
同一时刻对多个按键按下进行判断
系统滴答定时器(Systick)
*ADC的DMA多通道采样+平均滤波处理
*串口的DMA+空闲中断不定长接受任意类型的数据
定时器通道的输入捕获
运行期间动态更改PWM的频率
PWM波的生成
IIC读写epprom
扩展板之ds18b20温度传感器
扩展板之dht11温度传感器
时钟树
串口重定向:printf输出
注意:
- 勾选微库.若发现根本进不了main函数里面一般是这个原因.
- 包含#include "stdio.h"
int fputc(int ch,FILE * f )
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch,1,0xff);
return ch;
}
动态点灯(点灯大师)
u8 LEDINDEX[]= {0X00,1<<0,1<<1,1<<2,1<<3,1<<4,1<<5,1<<6,1<<7};
u8 LEDDT[]={0,0,0,0,0,0,0,0,0};
void led_display(u8 ds)
{
HAL_GPIO_WritePin(GPIOC,0xff<<8,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC,ds<<8,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
void led_scan(){
int ds = 0;
for(int i = 0 ;i < 4;i++)
{
ds|=LEDINDEX[LEDDT[i]];
}
led_display(ds);
}
运行期间只需要更改LEDDT的值就行,LEDDT没有顺序,表示最多同时点亮的灯的数目。
按键(常用状态机)
(状态机,假设同时只有一个按键按下,无论长按还是短按都只判断按下,记作一次)这个代码只能一次性判断一个按键按下,无法对同一时刻多个按键按下进行判断,一般这个代码就够用了)
使用定时器7,配置为20ms进入一次回调函数。这种我认为是日常中比较常用的按键操作,因为以前对51单片机按键扫描的记忆,故回顾了下。
#define ReadB1 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)
#define ReadB2 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)
#define ReadB3 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)
#define ReadB4 HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)
u8 key_state= 0;
u8 rkey = 0;
u8 key = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2)
{
LedFlusht++;
if(time++ == 1000)
{
time = 0;
LEDDT[1] = LEDDT[1]?0:1;
}
}
else if(htim->Instance == TIM7)
{
//执行按键扫描
switch(key_state)
{
case 0:
if(!ReadB1 || !ReadB2 ||!ReadB3 || !ReadB4)
{
key_state = 1;
}
break;
case 1:
if(!ReadB1 || !ReadB2 ||!ReadB3 || !ReadB4)
{
key_state = 2;
if(!ReadB1) key = 1;
else if(!ReadB2) key=2;
else if(!ReadB3) key=3;
else if(!ReadB4) key =4;
rkey = key;
}
else
{
key_state = 0;
key = 0;
}
break;
case 2:
rkey = 0;//在只需要判断按下之后,不管长按短按(有没有松开)都试做按下一次
if(!ReadB1 || !ReadB2 ||!ReadB3 || !ReadB4)
{
}
else
{
key = 0;
key_state= 0;
}
break;
}
keyAction();
}
}
同一时刻对多个按键按下进行判断
原理:把按键定义成一个结构体变量:
typedef struct Key{
u16 keytime;
u8 keystate;
bool sflag;
bool lflag;
}Key;
按键扫描:(按下小于500ms属于短按,大于500ms属于长按) ,按键扫描函数单独用一个定时器,每10ms产生一次中断去执行,这也就是为什么keytime+=10的原因.
#define ReadB1 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)
#define ReadB2 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)
#define ReadB3 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)
#define ReadB4 HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)
void key_scan()
{
keys[0].state = READB1;
keys[1].state = READB2;
keys[2].state = READB3;
keys[3].state = READB4;
for(int i = 0 ; i < 4 ;i++)
{
switch(keys[i].keystate)
{
case 0:
if( !keys[i].state){
keys[i].keystate =1;
}
break;
case 1:
if(!keys[i].state){
keys[i].keystate =2;
}
else
keys[i].keystate = 0;
break;
case 2:
if(!keys[i].state)
{
keys[i].keytime+=10;
}
else
{
if(keys[i].keytime > 500)
{
keys[i].lflag = 1;
}
else
keys[i].sflag = 1;
keys[i].keytime = 0;
keys[i].keystate=0;
}
break;
default:
break;
}
}
}
//串口测试代码
void keywork(void)
{
if(keys[0].sflag)
{
printf("0\r\n");
keys[0].sflag = 0;
}
else if(keys[0].lflag)
{
printf("long 0\r\n");
keys[0].lflag = 0;
}
if(keys[1].sflag)
{
printf("1\r\n"); keys[1].sflag = 0;
}
else if(keys[1].lflag)
{
printf("long 1\r\n");
keys[1].lflag = 0;
}
if(keys[2].sflag)
{
printf("2\r\n");
keys[2].sflag = 0;
}
if(keys[3].sflag)
{
printf("3\r\n");
keys[3].sflag = 0;
}
}
逻辑处理: 处理完后sflag (短按标志)或lflag(长按标志)记得清零处理。
系统滴答定时器(Systick)
HAL_GetTick() 函数是 STM32 微控制器 HAL(硬件抽象层)库提供的函数。它返回当前以毫秒为单位的节拍计数。默认情况下,系统定时器每1ms生成一个中断以更新节拍计数。因此,在大多数情况下,每个节拍的速率为1ms。
此函数可用于实现基于时间的操作,例如延迟、超时和任务调度。通过比较当前节拍计数与之前保存的值,可以确定自那个事件以来经过的时间。
例如,要创建100ms的延迟,您可以使用 HAL_GetTick() 保存当前节拍计数,然后在循环中等待,直到保存的节拍计数和当前节拍计数之间的差达到100ms。
uint32_t startTick = HAL_GetTick(); while ((HAL_GetTick() - startTick) < 100);
请注意,当节拍计数达到其最大值(0xFFFFFFFF)后,它会绕回,并且在计算长时间内经过的时间时必须考虑到这一点。
我们使用DHT11,DS18B20需要用到的us级延时就需要通过systick来实现。
HAL_GetTick(); 获取当前节拍.默认是1ms产生1个tick(节拍),则systick的计数值1ms发生溢出产生中断,
systick的默认中断函数:(每次中断溢出则增加节拍 ”Tick“)
Systick的主频:
SysTick定时器的主频取决于所使用的系统时钟(HCLK)频率和它的分频系数。在STM32微控制器中,SysTick定时器的时钟源可以配置为HCLK/8或HCLK。
如果SysTick定时器的时钟源被配置为HCLK/8,则SysTick定时器的主频将为HCLK/8。例如,如果HCLK的频率为72MHz,则SysTick定时器的主频将为9 MHz。
如果SysTick定时器的时钟源被配置为HCLK,则SysTick定时器的主频将为HCLK。例如,如果HCLK的频率为72MHz,则SysTick定时器的主频将为72 MHz。
需要注意的是,SysTick的主频越高,计数器溢出的时间间隔就越短,因此可以获得更高的精度和分辨率。但是,SysTick的主频和计数器位数的组合也会影响 SysTick 的最大定时周期。
systick的时钟源一般被设置为AHB总线上的时钟频率,比如常见的80MHZ.
配置systick的时钟频率 HAL_SYSTICK_CLKSourceConfig()
两种选择 HCLK 或 HCLK/8
溢出时间判断:SYSTICK->LOAD的装载值为79999,所以:
产生一次中断的时间为:1/(80M-1)*(79999+1) =1/10^3 = 1ms
默认的SYSTICK中断函数:
/** * @brief This function handles System tick timer. */ __weak void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); /* USER CODE BEGIN SysTick_IRQn 1 */ /* USER CODE END SysTick_IRQn 1 */ } /** * @brief This function is called to increment a global variable "uwTick" * used as application time base. * @note In the default implementation, this variable is incremented each 1ms * in SysTick ISR. * @note This function is declared as __weak to be overwritten in case of other * implementations in user file. * @retval None */ __weak void HAL_IncTick(void) { uwTick += uwTickFreq; }
把SYSTICK做定时用,重写systick的中断函数.(节省定时器,一般情况下,我们都不会这样去做,因为OS会把它作为时基,而我们的一些ms级延时也要得益改变systick的频率得到)
此时需要把原来的SysTick_Handler标记为weak。不好的地方在于每次生成代码都需要改.
这一点纯属当拓展知识去用,知道可以这么干就可以了.
void SysTick_Handler(void)
{
HAL_IncTick();
static uint32_t ick = 0; // 定义静态变量tick,记录自系统启动以来经过的毫秒数
ick++; // 每次中断触发时tick值加1
if (ick == 1000) { // 如果tick值等于1000,则表示已经过了1秒钟
// TODO: 执行每秒钟需要执行的任务
ick = 0; // 将tick值重置为0,开始新的计数
printf("hello world\r\n");
}
}
*ADC的DMA多通道采样+平均滤波处理
比赛实际上用单通道就可以,每次采样需要指定adc的通道.
STM32基于hal库的adc以DMA的多通道采样以及所遇问题解决_stm32 hal adc dma_昊月光华的博客-CSDN博客
*串口的DMA+空闲中断不定长接受任意类型的数据
(记得数据宽度设置为1个字节 8位)
若是hal库的版本并不是特别的新,那么还是用串口接受中断+空闲中断吧,我这边测试1.3的固件包是可以用DMA的那个空闲中断函数 ,如:
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,Rx1Buf,200); //串口1开启DMA接受
基于HAL库的STM32的串口DMA发送数据(解决只发送一次数据)及DMA+空闲中断接受数据_hal 串口dma发送_昊月光华的博客-CSDN博客
定时器通道的输入捕获
对输入的脉冲进行捕获,算出求频率和占空比.设置定时器的频率的1M用来对输入的脉冲进行计数,假设输入脉冲一个周期的计数值为t(从一个脉冲的上升沿到下一个脉冲的上升沿).则频率计算为 1000000 / t .
比如对PA15信号发生器.
配置(PA15接脉冲发生器测出来的占空比大概为50%。
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
static u16 t = 0;
static u16 d = 0;
if(htim->Instance == TIM2)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
LEDDT[0]=1;
t = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1)+1;
freq = 1000000 / t;
duty = (float)d/t*100;
}
else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
LEDDT[1]=2;
d = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2)+1;
}
}
}
通过按键打印
运行期间动态更改PWM的频率
pwm的频率 = 定时器的频率/autoreload的值.其中定时器的频率 = 时钟频率/分频系数.
之前参加过蓝桥杯单片机的相信有过经历:用定时器去实习pwm波:
记得我们是如何实现的?
假设定时器的频率是1M.每1us计数值加1,若设置为1000个计数值后溢出则产生一次中断,则1ms进入一次中断,在中断函数内,我们让其定义一个变量,让其加到200后归0,其中前100ms设置为高电平,后100ms设置为低电平,则周期为200ms,频率为1/200ms=5HZ,占空比为50%。(假设有效电平为高电平).
再回到STM32来,这里的autoreload就是计数值,一个计数的时间是1/定时器的频率。则PWM的频率为1 / [autoreload*1/定时器的频率) ] = 定时器的频率/ autoreload .证明完毕.
我们通过更改autoreload的值去动态的更改pwm的频率,需要注意的是当autoreload的值发生改变,需要重新去计算占空比。 占空比 = TIM15->CCR1 /autoreload .
//改变PWM的频率且稳定占空比保持不变 以TIM15->CCR1为例
void changePwmFreqAndkeepDuty(float duty,u16 autoloadval)
{
extern TIM_HandleTypeDef htim15;
TIM15->CCR1 = duty*autoloadval;
printf("new freq: %d\r\n",10000/autoloadval);
__HAL_TIM_SetAutoreload(&htim15,autoloadval-1);
HAL_TIM_GenerateEvent(&htim15, TIM_EVENTSOURCE_UPDATE);
}
通过按键触发更改
按键触发的demo
if(keys[0].sflag)
{
static u8 t =0;
t++;
static bool flag = false;
if( t == 100) t=0;
void changePwmFreqAndkeepDuty(float duty,u16 autoloadval);
flag =!flag;
if(flag) changePwmFreqAndkeepDuty(0.8,50);
else changePwmFreqAndkeepDuty(0.5,100);
printf("0\r\n");
keys[0].sflag = 0;
}
PWM波的生成
相信看到这,PWM的产生原理已经很清楚了,用cubeMX配置将会更加清楚.
一个定时器可以配置多个通道产生多组PWM波.
开启PWM波:
HAL_TIM_PWM_Start_IT(&htim15,TIM_CHANNEL_1);
更改PWM的值(如TIM15) TIM15->CCR1 =XXX (表示TIM15的通道1)
IIC读写epprom
需要注意在读数据时,需要主机发送应答信号表示。以及需要初始化.
I2CInit(void);
//EEPROM读写代码
void eeprom_write(unsigned char *pucBuf, unsigned char ucAddr, unsigned char Size)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(ucAddr);
I2CWaitAck();
while(Size--)
{
I2CSendByte(*pucBuf++);
I2CWaitAck();
}
I2CStop();
}
void eeprom_read(unsigned char *pucBuf, unsigned char ucAddr, unsigned char Size)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(ucAddr);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
while(ucNum--)
{
*pucBuf++ = I2CReceiveByte();
if(Size)
I2CSendAck();
else
I2CSendNotAck();
}
I2CStop();
}
扩展板之ds18b20温度传感器
驱动代码比赛官方会给出.只需要自己写一个读取温度的函数就行.给出的驱动有个不稳定的因素,那就是delay_us()这个延时函数没有重写。驱动是以80M做的一个微秒级延时,当在更改系统时钟频率时则有大问题(因为指令周期会随着系统的时钟频率而变化!),同样的,DHT11的驱动也有这个问题.,这完全是让学生专注于与逻辑业务的实现,驱动能跑就行(记得设置系统主频为80M).
资源包给的延时函数:
#define Delay_us(X) delay((X)*80/5)
void delay(unsigned int n)
{
while(n--);
}
跳线帽连接 P4 与 P3 的 TDQ进行连接.
main函数中调用初始化:
ds18b20_init_x();
读取温度
float ds18b20_read(void)
{
unsigned char th,tl;
unsigned short res;
ow_reset();
ow_byte_wr(0xcc);
ow_byte_wr(0x44);
ow_reset();
ow_byte_wr(0xcc);
ow_byte_wr(0xbe);
tl = ow_byte_wr(0xff);
th = ow_byte_wr(0xff);
res=(th<<8)|tl;
return res*0.0625;
}
扩展板之dht11温度传感器
每次读取完后的下一次读取则会失败,这不是我们的问题。只需要得到在它正确读取时的值就行。
dht11比赛赛点资料包(14届)给的驱动代码只给了2个函数,两个改变DQ输入输出的函数。。。
dht11_hal.h
#ifndef __DHT11_HAL_H
#define __DHT11_HAL_H
#include "stm32g4xx_hal.h"
#define HDQ GPIO_PIN_7
typedef struct {
float temp;
float humi;
}dht11Data;
void dht11Init(void);
int Readdht11(void);
#endif
dht11_hal.c
#include "dht11_hal.h"
#define OUTDQLOW HAL_GPIO_WritePin(GPIOA, HDQ, GPIO_PIN_RESET)
#define OUTDQHIGH HAL_GPIO_WritePin(GPIOA, HDQ, GPIO_PIN_SET)
#define READDQ HAL_GPIO_ReadPin(GPIOA,HDQ)
dht11Data dht11;
//
static void usDelay(uint32_t us)
{
uint16_t i = 0;
while(us--){
i = 30;
while(i--);
}
}
//
void outDQ(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Pin = HDQ;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);
}
//
void inDQ(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.Pin = HDQ;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);
}
//
void dht11Init(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
outDQ();
OUTDQHIGH;
}
//
uint8_t recData(void)
{
uint8_t i,temp=0,j=220;
for(i=0; i<8; i++){
while(!HAL_GPIO_ReadPin(GPIOA,HDQ));
usDelay(40);
if(HAL_GPIO_ReadPin(GPIOA,HDQ))
{
temp=(temp<<1)|1;
while(HAL_GPIO_ReadPin(GPIOA,HDQ)&&(j--));
}
else
{
temp=(temp<<1)|0;
}
}
return temp;
}
//复位DHT11
void DHT11_Rst(void)
{
outDQ(); //设置为输出
OUTDQLOW;
HAL_Delay(20); //拉低至少18ms
OUTDQHIGH;
usDelay(60); //主机拉高20~40us
}
//等待DHT11的回应
//返回1:未检测到DHT11的存在
//返回0:存在
unsigned char DHT11_Check(void)
{
unsigned char re = 0;
inDQ(); //设置为输入
while (READDQ && re < 100) //DHT11会拉低40~80us
{
re++;
usDelay(1);
};
if(re >= 100)return 1;
else re = 0;
while (!READDQ && re < 100) //DHT11拉低后会再次拉高40~80us
{
re++;
usDelay(1);
};
if(re >= 100)return 1;
return 0;
}
int Readdht11(void)
{
unsigned char buf[5];
unsigned char i;
DHT11_Rst();
if(DHT11_Check() == 0)
{
for(i = 0; i < 5; i++)
{
buf[i] = recData();
}
if((buf[0] + buf[1] + buf[2] + buf[3]) == buf[4])
{
dht11.humi = buf[0];
dht11.temp =buf[2];
}
}
else return 1;
return 0;
}