创建工程相关事宜

这个选项配置应该是选择下载调试方法;用这款单片机开发板的话,就应该是这个样子配置,但是原先配置的是ST-link的,这个方法应该就是用ST-Link的下载器下载程序的,

RCC时钟

记住按回车保存时钟树的数据(上图) ,不然这个模板复制之后时钟树的数据并不会保存下来

出现头文件未找到的问题

1:首先检查创建的时候是否放在的指定的文件夹,(这里我用的是江科大的创建文件的方法)

2:再在魔法棒->C/C++里面看看是否把文件夹的路径添加进来

3:还有就是看看三个箱子选项;看看是否文件夹里面有对应的.c和.h文件

4:上面三个都没有问题的话,就去魔法棒里面的c/c++,把对应的文件夹都删了,在创建一个新的,再到主界面双击这个文件夹,再把.c和.h文件添加回来;不要创建,要添加进来,应该就是说要在外面创建一个zzuser专属文件夹,然后在里面添加进来;

LED

原理图

注意点:

1:74HC573锁存器,只要高电平使能这个锁存器就可以了,

2:但是在只点亮一个的时候,发现八个灯都点亮了,而且每次都是其他灯随机点亮的,推测是其他的引脚未设置数入模式,未设置的时候就应该是默认浮空输入,就会出现随机亮灯的情况,我的解决方法是,在MX里面给每个LED灯的引脚都设置为上拉输入,这个就不会出现随机亮灯的情况。

闪烁灯

点亮1位灯的封装函数 

void LED_Display_Bit(uint8_t LED, uint8_t state)  //LED参数为LED位号, state参数为LED状态,1亮0灭
{
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
	
	if(state)
	{
		HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8<<(LED-1), GPIO_PIN_RESET);
	}
	else 
	{
		HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8<<(LED-1), GPIO_PIN_SET);
	}
	
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}

键盘Key

简单阻塞按键

#include "stm32f1xx_hal.h"                  // Device header

uint8_t KeyNum;

void Key_GetNum(void)
{
	if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == 0)
	{
		HAL_Delay(20);
		while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == 0);
		KeyNum = 4;
	}
}

非阻塞式按键(只能检测单击 )

1:利用系统时间戳消抖,

2:我的理解
在第一次按键按下后,last_state会被置为1,因为这个循环的时间发生的很快,第二次很快就检测到按键的现状态为1了,
因为没有按键按下,所以按键现状态为1;
然后判断if(now_state != Key[i].last_state)  //判断现状态与旧状态是否相同,肯定是不一样的,因为上一个状态是按键按下,
接着就会更新旧状态为1,之后的消抖过程也会发生,稳定状态也会随之更新为1,为下一次按键按下做准备,
但是到了最后一步,检测按键的现状态是否为0(就是说按键是否按下),按下才会输出按键值

#define Key_delay_time 20 

typedef struct{

GPIO_TypeDef * Port;  //GPIOX 
uint16_t  pin;  //引脚号
uint8_t last_state;  //按键的旧状态
uint8_t stable_state;  //按键消抖后的稳定状态	
uint32_t last_time;  //旧的时间戳的值,用来按键消抖	
	
}Key_habdle;

Key_habdle Key[4]=
{
	{GPIOB, GPIO_PIN_0, 1, 1, 0}, //Key1
	{GPIOB, GPIO_PIN_1, 1, 1, 0}, //Key2
	{GPIOB, GPIO_PIN_2, 1, 1, 0}, //Key3
	{GPIOA, GPIO_PIN_0, 1, 1, 0}  //Key4
};
 

uint8_t Get_KeyNum(void)
{
	uint8_t Keynum = 0;  //按键的键码值 
	uint8_t now_state = 0;  //按键的现状态
	uint32_t now_time = HAL_GetTick();  //获取现在的时间戳
	
	for(uint8_t i=0; i<4; i++)  //循环扫描按键的状态
	{
		now_state = HAL_GPIO_ReadPin(Key[i].Port, Key[i].pin);  //获取当前的按键状态
		
		if(now_state != Key[i].last_state)  //判断现状态与旧状态是否相同
		{
			Key[i].last_time = now_time;  //记录下现在的时间,为之后的消抖做准备
			Key[i].last_state = now_state;  //
		}
		
		if(now_time - Key[i].last_time > Key_delay_time)  //按键的消抖
		{
			
			if(Key[i].stable_state != now_state)  //判断现状态与稳定状态是否相同
			{
				
				Key[i].stable_state = now_state;  //更新稳定状态
				
				if(now_state == 0)  //这步最关键,检测按键是否按下,按下才能输出按键键码值
				{
					Keynum = i+1;
				}
			}
			
		}
	}
	return Keynum;
}

/*
在第一次按键按下后,last_state会被置为1,因为这个循环的时间发生的很快,第二次很快就检测到按键的现状态为1了,
因为没有按键按下,所以按键现状态为1;
然后判断if(now_state != Key[i].last_state)  //判断现状态与旧状态是否相同,肯定是不一样的,因为上一个状态是按键按下,
接着就会更新旧状态为1,之后的消抖过程也会发生,稳定状态也会随之更新为1,为下一次按键按下做准备,
但是到了最后一步,检测按键的现状态是否为0(就是说按键是否按下),按下才会输出按键值

*/

非阻塞式按键(只能检测单击 )(江科大版本)

#include "stm32g4xx.h"                  // Device header

#define Key_Delay_Time 20


uint8_t Num; 

uint8_t Key_GetNum(void)
{
	uint8_t Temp = 0;
	
	if(Num)
	{
		Temp = Num; 
		Num = 0;
	}
	return Temp;
}


uint8_t  Key_Getstate(void)
{
	uint8_t state = 0;
	
	if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)  //Key1
	{
		state = 1; 
	}
	else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)  //Key2
	{
		state = 2; 
	}
	else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET)  //Key3
	{
		state = 3; 
	}
	else if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)  //Key4
	{
		state = 4; 
	}
	return state;
}

void Key_Tick(void)
{
	static uint8_t new_state = 0;
	static uint8_t old_state = 0;
	static uint8_t count = 0;
	
	count++;
	if(count>=Key_Delay_Time)
	{
		count = 0;
		
		old_state = new_state;
		new_state = Key_Getstate();
		
		if((old_state == 0)&&(new_state == 1))
		{
			Num = 1; 
		}
		else if((old_state == 0)&&(new_state == 2))
		{
			Num = 2; 
		}
		else if((old_state == 0)&&(new_state == 3))
		{
			Num = 3; 
		}
		else if((old_state == 0)&&(new_state == 4))
		{
			Num = 4; 
		}

	}
	
	
}

 易错点

1:代码状态错误

void LED_Pro(void)
{

	static uint8_t temp = 0;
	
	Key_GetNum();
	
	if(KeyNum == 4)
	{
		KeyNum = 0;

		LED_Dispaly(~temp);//状态切换逻辑缺失​:按下按键时仅仅显示 ~temp,但未更新 temp 的值,导致每次显示的可能是随机值或无变化。
		
		

	}
}

        状态切换逻辑缺失​:按下按键时仅仅显示 ~temp,但未更新 temp 的值,导致每次显示的可能是随机值或无变化。

下面是正确的方法;
 

// 

 2.LED状态

 这个状态用完之后要快速的清零,如果是全局变量的话,还要用完之后就清零,防止状态一直不更新清零,导致后面的定时器里面的检测状态不起效,所以一般就是用局部变量的,如果要全局变量的就是要用完之后要清零;

//

LCD屏幕

注意点:

1:引脚使用了PA8,PB5,PB8,PB9,PC0~PC15;

基本的初始化和显示函数

解决LCD与LED的引脚冲突问题

1:首先就是在操作LED的时候,在给LED的引脚赋值的时候,先使能573寄存器,在操作完LED的引脚之后呢,在失能573锁存器:代码示例如下;

void LED_Display_Bit(uint8_t LED, uint8_t state)  //参数LED为LED位号,state为状态,0亮1灭
{
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);//使能573锁存器
	
	if(!state)
		HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8<<(LED-1), GPIO_PIN_RESET);
	else 
		HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8<<(LED-1), GPIO_PIN_SET);
	
	HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);//失能573锁存器
}

 2:但是有时候的按键还是会与LCD冲突;这个时候直接在用到的LCD函数里面加一个下面这个操作,先把输出端的状态存起来,在LCD操作完PC端口后,再把之前存的状态值复原给输出端,这个样子无论LCD怎么改变PC口,PC口都会复原成改变之前的样子,

有LCD的地方都要

 只需要在出现LCD函数的头尾加上这两话就可以了

封装LCD函数(可变参数)

c语言可变参数

简单求和函数 

 

LCD封装函数

#include "stm32g4xx.h"                  // Device header
#include "lcd.h"
#include "stdio.h"

void zz_LCD_Init(void)   //LCD初始化
{
	LCD_Init();
	LCD_SetTextColor(White);
	LCD_SetBackColor(Black);
	LCD_Clear(Black);
}


#include <stdarg.h>  // 包含可变参数支持头文件

void zz_LCD_DisplayStr(uint8_t LineX, const char *format, ...)
{
    char buffer[64];  // 足够大的缓冲区存储格式化后的字符串
    
    // 设置可变参数列表
    va_list args;
    va_start(args, format);
    
    // 将格式化字符串和参数写入缓冲区
    vsnprintf(buffer, sizeof(buffer), format, args);
    
    // 结束可变参数使用
    va_end(args);
    
    // 在指定行显示格式化后的字符串
    LCD_DisplayStringLine(LineX, (uint8_t *)buffer);
}

 

 

定时器

 系统滴答定时器

系统滴答定时器实现流水灯,这个定时器是1毫秒就进一次中断,

设置PSC预分频器和ARR自动重装载值

 开启NVIC

 设置好上面的参数,文件就会自己产生一个定时器文件,

注意点:

在上面这个文件夹下会有一个使能中断的函数 

再这个文件夹下就会生成一个中断服务函数 

        这个中断函数好像不能移到其他的地方,但是我感觉只要把原来的那个中断函数给注释掉,就可以把这个中断函数挪位置,

        最好不用这个上面的函数,最好用封装出来的回调函数

 上图,上面这个函数是开启定时器的,下面的这个函数是开启中断的,如果需要开启中断的话,就只需要用一下下面的这个函数就可以了

中断回调函数

简介

​**HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)**

​**HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)**​ 

**HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)**​ 

 ​**HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)**​

 

 ​**HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim)**​

 ​**HAL_TIM_ErrorCallback(TIM_HandleTypeDef *htim)**​

 用法:

注意点

PWM

PWM输出比较-输出频率

cubeMX配置

 

注意:这个设置占空比的,应该来说设置为0,也没有问题,但是在设置PC8为呼吸灯的时候就不能设置为0,我觉得应该是设置为0的时候就直接没有频率,全都是第低电平哪里来的频率,一般不做要求的话,ARR设置为100-1;上面的占空比设置为50;

 Keil代码

首先就是PWM输出频率的话,就要开启这个函数;

 上图的函数是设置PWM使能的,但是说你如果要使用中断的话,还需要设置PWM的中断,应该这个中断函数是带着IT的,

 然后如果需要改变频率的就可以用操作寄存器的方法;上图,但是我们只需要改变PSC就可以改变频率,所以我们进入这个函数里面:

操作PSC寄存器就可以直接的改变分频值 ;

下面是一些其他的方法,

 其中的两个函数的参数,小小的看一下,具体不会的话可以Ai;

还有一点就是如果用的是蓝桥杯的板子,记得使能573锁存器

PWM输入捕获测量频率

上图:

1:基准时间t0是一个频率跳变的时间,

2:capture_value是每两个上升沿之间的时间;单位是毫秒;

3:然后就能得到周期T为基准时间t0乘以capture_value;

4:频率就为周期的倒数;带入就得到上面的公式,fsys是系统频率80MHz,psc就是分频系数

 

下面是各个参数的解释: 

 

 

 在设置完上面的设置后,最后记得开启NVIC中断

 有一个小细节:死活找不到Tim.h文件的话,点击编译之后就会出来了

#include "stm32g4xx.h"                  // Device header



uint32_t Freq;

uint32_t count = 0;

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{


	if(htim->Instance==TIM3)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
		{
		  count = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
			
			Freq = 1000000/count;
			count = 0;
		}
	}
}

        但是感觉还是有问题,最高7万多,最低只有1万多,但是刚复位的时候,就只有700多,之后就没有最低700多了

         经过验证是正确的

 这个函数HAL_TIM_ReadCapturedValue(),在Stm32g4xx.hal.tim.h文件夹里面

555定时器-双通道测量频率

        这个555定时器输出的频率测量起来还跟上面的PWM输入捕获测量频率不一样,但是他们都是测量频率,为什么会不一样,在csdn上面别人的MX配置方法好像跟又是一样的;

        测量单个555定时器的时候好像又是一样

cubeMX配置

下面是PA15的

再勾选上NVIC。

下面的是PB4的;

上面的cube和代码还是有问题,

ADC测量

简介

 读取一路ADC值

cubeMX配置

cubeMX只需要简单得使能一下ADC

Keil代码编写

ADC读取函数

 显示函数

 注意点

1:extern修饰的变量只能是全局变量

USART串口

简介:

原理图

代码:

发送

cubeMX配置

 单单是发送的话,就用不到NVIC中断;

 keil代码
void zz_UART_Send_Pro(void)
{
	char UART_Text[20];
	
	sprintf(UART_Text, "Hello World\r\n");
	HAL_UART_Transmit(&huart1, (uint8_t *)UART_Text, sizeof(UART_Text), 50);
	HAL_Delay(1000);
}

发送与接收: 

cubaMX配置

接收与发送相比,接收用到了串口中断;

所以要多配置一下NVIC

Keil代码
#include "stm32g4xx.h"                  // Device header
#include "stdio.h"
#include "usart.h"

uint8_t UART_Recieve_data;

void zz_UART_Init(void)  //这个初始化函数放在while大循环的外面
{
	HAL_UART_Receive_IT(&huart1, &UART_Recieve_data, 1);

}

void zz_UART_Send_Pro(void)  //发送的函数
{
	char UART_Text[20];
	
	sprintf(UART_Text, "Hello World\r\n");
	HAL_UART_Transmit(&huart1, (uint8_t *)UART_Text, sizeof(UART_Text), 50);
	HAL_Delay(1000);
}


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)  //接收中断回调函数
{
	if(huart-> Instance == USART1)
	{
		HAL_UART_Transmit(&huart1, &UART_Recieve_data, 1, 50); //
		
		HAL_UART_Receive_IT(&huart1, &UART_Recieve_data, 1);  //
	}
}

我的理解是:

HAL_UART_Receive_IT(&huart1, &UART_Recieve_data, 1);这个函数是接收的中断使能函数,在初始化的时候调用一下,这个时候就已经准备好接收了,创建了一个地址,这个地址就已经准备好接收数据了,相当于这个函数调用一次,这个地址就能接收一次数据,当串口接收到一个数据的时候,会直接把数据发送到这个地址内,这个时候程序就会到了串口回调函数里面的,在把数据回显,然后就到了中断回调函数里面的HAL_UART_Receive_IT(&huart1, &UART_Recieve_data, 1);,再一次准备好接收;

利用定时器进行不定长数据的接收

注意点:

1:中断回调函数的使用

 要谨慎使用这个函数,最好直接用她的回调函数;

2:串口数值传输的形式

一般我们在传输给单片机的一个‘1’,其形式是按照ASCII码表来传输的,1对应的码值是0x49;

在LCD上显示的话,直接显示就会显示一个49;这个时候有两个方法:

        a:直接转换为字符显示

        b:从ASCII码减去一个字符零,也就是48,然后进行显示;

                这个有个前提:要保证这个字符值>'0';

        

日志:

EEPROM

简介:

RTC实时时钟

简介

代码

设置与读取时间

cubeMX配置

keil代码

易错点

1:

注意定义时间和日期的结构体是不一样的,获取时间和日期的函数也是不一样的

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐