前言

网站

Keil MDK 官网

芯片包-暂选1.2版本的

ChatGPT

参考文章

STM32-蓝桥杯嵌入式LCD字符颜色、高亮显示

【蓝桥杯单片机最全备考资料】真题、代码、原理图、指导手册、资源包(不是嵌入式的是单片机组)

蓝桥杯嵌入式STM32G431RBT6程序-github

按键三行核心代码解决按键按下、松开、长按-代码高效移植快-百度文库

2022年参加蓝桥杯嵌入式比赛——历年赛题训练-github

蓝桥杯嵌入式备赛-指导手册(编写自常州信职学生翟宇涵)

三行代码按键消抖 独立按键 矩阵按键 长按 短按 双击

STM32-DAC生成1Hz三角波【DAC触发方式深入理解】

STM32-实现us延时

蓝桥杯 stm32 MCP4017

【STM32+cubemx】0009 HAL库开发:RTC实时时钟的使用、掉电时间保持

stm32HAL库 RTC配置并设置闹钟间隔响铃(思路+具体方法)

STM32CubeMX配置PWM+DMA以及实现代码

STM32HAL库输出精确数量PWM波遇到的问题

利用定时器输出比较模式的翻转功能实现不同占空比和频率的PWM输出

【STM32】HAL库 STM32CubeMX教程十一—DMA串口DMA发送接收

STM32 - 随笔分类 - jym蒟蒻 - 博客园

蓝桥杯嵌入式省赛【耗不尽的先生】

订阅栏汇总

历年题目

第十三届蓝桥杯嵌入式国赛真题

软件/芯片包准备

Keil MDK 5.36-最新版也行

CubeMAX 6.4.0

STM32G4xx_DFP.1.2.0.pack

我的综合模块源码工程

板子资源介绍

主控芯片: STM32G431RBT6

序号 资源
1 Type-C USB 转串口/调试接口比赛板是Type-B 1路
2 自锁开关 1个
3 复位按键 1个
4 TFF-LCD 2.4寸
5 功能按键 4个
6 LED灯 8个
7 分压电位器 2个
9 信号发生器 2路
11 蜂鸣器已取消 1个
12 Type-C USB 接口(USB设备接口) 1个
13 可编程电阻 1个(100K)
14 EEPROM AT24C02
17,18 扩展接口(J1,J3) 2排
19 微控制器 STM32G431RBT6
20 板载调试器 CMSIS DAP Link
8,16 拨码开关(跳线帽)功能选择 3组

12个拨码开关(跳线帽组)如下表(用于选择不同功能,初学时一般全部连接状态,默认已经全部连接的)

序号 跳线帽组 功能说明 备注
1 J6 调试器/SWCLK 断开后调试器不可用
2 J7 调试器/NRST 断开后调试器不可用
3 J8 调试器/SWDIO 断开后调试器不可用
4 J9 脉冲输出/PB4
5 J10 脉冲输出/PA15
6 J11 电位器分压/PB15
7 J12 电位器分压/PB12
8 J13 USB D+/PA12 断开后USB设备不可用
9 J14 USB D-/PA11 断开后USB设备不可用
10 J15 可编程电阻 W
11 J16 可编程电阻 B
12 J17 参考电源/VREF+ 断开后AD检测异常

程序下载调试需要连接 CN2 接口(调试器的USB 转串口功能默认与STM32G431RBT6 微控制器 USART1 连接)

核心板功能

名称 内容
芯片型号 STM32G431RBT6
频率(MHz) 170
内核 Cortex-M4
闪存Flash(KBytes) 128
内存SRAM(KBytes) 32
E2prom(Byte) 0
电源电压(v) 1.71-3.6
定时器 类型
TIM6,TIM7 基本定时器
TIM2~TIM4 全功能通用定时器
TIM15~TIM17 通用定时器(只有1个或者2个通道)
TIM1和TIM8 高级控制定时器

基础CubeMX配置

➢ 以下配置适用于本文的所有模块(一般只设置一次不需要再改,如需要改会在对应模块那注明)

  • 先配置下载调试接口

  • 然后开启时钟——外部高速时钟

  • 配置时钟树,官方要求80MHZ
  • 为什么外部晶振是24MHz,可在 system_stm32g4xx.c大概81行可以找到 外部振荡器的值是 24000000U,也可以在原理图里看

  • 文件配置

第一次点击生成会弹出需不需要下载固件包的警告,点击 Yes 即可(我的固件包为 stm32cube_fw_ g4_v150.zip),下载完成点击 Open Project 将在 Keil MDK 中打开工程

  • Keil的基本配置

创建一个 APP 文件夹存放模块的 .c/.h 方便管理,添加进来后记得在路径那把头文件也添加进来!

把官方例程 LCD,II2相关的也一起加进工程

魔法棒C/C++ — 查看 Define 是不是:USE_HAL_DRIVER,STM32G431xx(一般默认已经填好的)

检查配置的时钟源的频率对不对,在main函数里添加:(结果应该全是80MHz)

uint32_t SysClk,HClk,PClk1,PClk2;

SysClk = HAL_RCC_GetSysClockFreq();
HClk = HAL_RCC_GetHCLKFreq();
PClk1 = HAL_RCC_GetPCLK1Freq();
PClk2 = HAL_RCC_GetPCLK2Freq();

用户自己编写的代码注意写在区域里,不然重新生成 CubeMX 会没了

LED

硬件连接

管脚 对应 模式
PC8-PC15 LD1-LD8 初始高电平,推挽输出,无上下拉
PD2 LE 初始低电平,推挽输出,无上下拉

573锁存器:PD2高电平使能锁存器,可以 保留D1~D8八个引脚的状态(只有在锁存器使能情况下才保存);并且让Q1~Q8一直输出此状态(无论使能锁存器还是不使能锁存器,都输出原先的状态)

CubeMX配置

程序编写

uint8_t LED_state = 0XFF;	//LED状态


//一般会这个就行了,点亮任意一个:0x01~0x80 熄灭是0xFF
//这种安全不会影响到其他LED和LCD,推荐!!!
void LED_Write_all(uint8_t data)
{
	uint16_t a;
	a=GPIOC->ODR;
	GPIOC->ODR=(uint16_t)data<<8;
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
	GPIOC->ODR=a;
}

//闪烁
void LED_function(void)
{
    if(0 == get_time_task(TASK_TIMES.TASK_LED))	//200ms
    {
        TASK_TIMES.TASK_LED = get_time() + LED_TIME;
        if(Data.LED_RUN_Flag)
        {
            LED_state ^= 1;	//第一个LED,第2个就^2,第3个就^4
        }
        else
        {
            LED_state = 0xFF;
        }
        LED_Write_all(LED_state);
    }
}

//LED3亮使用步骤:
Led_State = (~0x04)&Led_State;	//LED3亮
LED_Write_all(LED_state);
//LED3灭使用步骤:
Led_State = 0x04|Led_State;	//LED3灭
LED_Write_all(LED_state);
//其他类似...不会影响到其他LED跟闪烁也不会影响

按键

硬件连接

管脚 对应 模式
PB0 B1 输入,上拉
PB1 B2 同上
PB2 B3 同上
PA0 B4 同上

CubeMX配置

  • 这里用到长按所以需要一个定时器(TIM2),选择 内部时钟源,定时1ms,开启 定时器中断

80×100080000000=0.001s=1ms\frac{80\times1000}{80000000} = 0.001s = 1ms



程序编写

按键有四种实现方式:普通消抖(太简单不写了),状态机外部中断(但是PA0和PB0只能有一个使用外部中断,因为它们中断线是一样),经典三行

状态机写法

  • 使用状态机的方法实现一个按键短按长按,剩下3个配置类似(用到定时器2计数)
/*************************KEY.h*************************/
# ifndef __KEY_H
# define __KEY_H
# include "main.h"


# define KEY1	HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)
# define KEY2	HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)
# define KEY3	HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)
# define KEY4	HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)

extern uint8_t KeyStart;	//状态
enum {
    Key1_No_Press,	//按键松开
    Key1_Press,	//按键按下
    Key1_Short,	//按键短按
    Key1_Long,	//按键长按
};


uint8_t Key_Scan(void);
void Key1_Mode(void);
void Key_Init(void);

# endif
/*************************KEY.c*************************/
uint8_t KeyStart = Key1_No_Press;
uint32_t KEY_Delay = 0;

void Key1_Mode(void)
{
    switch(KeyStart)
    {
		case Key1_Press:
		{
			Sys_Count = 0;	//计数清0
			while(!KEY1)
			{
				if(Sys_Count>=1000)	//判断定时器是否计数到1000则表示长按
				{
					KeyStart = Key1_Long;
					return;	//退出函数
				}
			}
            //如果执行到这表示短按
			KeyStart = Key1_Short;
			break;
		}
		case Key1_Short:	//短按对应操作
			lightAll_open(0);
			light1357_open(1);
			KeyStart = Key1_No_Press;
			break;
		case Key1_Long:	//长按对应操作
			lightAll_open(0);
			light2468_open(1);
			Key_Init();	//电平复位
			KeyStart = Key1_No_Press;	//改变状态
			break;
    }
}

//按键电平复位
void Key_Init(void)
{
    while(1)
    {
        if(KEY1 == 1)
            break;
    }
}
/*************************TIMER.c*************************/
uint32_t Sys_Count = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim == &htim2)	//如果是TIM2产生中断
	{
		Sys_Count++;
		if(KEY1 == 0)
		{
			KeyStart = Key1_Press;
		}
		# if 0
		if(Sys_Count >= 2147483647)	//不需要这句因为如果溢出需要连续不断电大概24天
		{
			Sys_Count = 0;
		}
		# endif
		
	}
}
/*************************main.c*************************/
int main(void)
{
  lightAll_open(0);	//一开始LED全灭
  HAL_TIM_Base_Start_IT(&htim2);	//使能定时器2+中断
    
  while(1)
  {
      # if 1
	  Key1_Mode();	//根据状态去执行对应操作
	  # endif
  }
}

外部中断写法

CubeMX配置:

PB0,PB1,PB1分别选择 EXTI0,EXTI1,EXT2模式,然后配置下降沿触发;上拉(结合原理图),然后在NVIC中打钩(即使能)对应外部中断



注意

回调函数内不需要清标志位,系统自动帮我们清除了(前提是使用CubeMX配置的外部中断)

/*************************EXTI.c*************************/
//外部中断函数在stm32g4xx_hal_gpio.c里面有弱函数定义
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == GPIO_PIN_0)	//如果是PB0按下
	{
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == 0)
		{
			light2468_open(1);
		}
	}
	if(GPIO_Pin == GPIO_PIN_1)	//如果是PB1按下
	{
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == 0)
		{
			light2468_open(0);
		}		
	}
	if(GPIO_Pin == GPIO_PIN_2)	//如果是PB2按下
	{
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == 0)
		{
			lightAll_open(1);
		}		
	}
	
}

三行短按长按(推荐)

key_value:存放读取按键的键值(相差10ms的临时值)

key_up:按键的上升沿检测 只在按键抬起的瞬间有效 其他时刻都为零无效(相当于松开)

key_down:按键的下降沿检测 只在按键按下的瞬间有效 其他时刻都为零无效(相当于按下)

key_old:记录上一次按键按下后的键值(相差10ms的临时值)

/*按键3行解析*/

/*
这一行就是去读取高低电平状态,高电平即使未按下,低电平是按下。这一行只能说明是一种状态,如果是最基本的按下触发,状态值是不可以使用,因为一次按键按下至少维持几十毫秒,再这几十个毫秒里程序会不只一次获取状态值,那么我们通过按键所执行的任务就会多次执行。
所以仅仅读取电平状态是不可行的,如何做到按键一次按下,只执行一次
*/
key_value = KEY_Scan();
/*
第二行既要获得下降沿又要获得按键键值,即哪个按键发送下降沿
key_value^key_old 将上次的电平状态与当前的电平状态按位异或,可以检测出跳变沿,没有跳变沿为0,有跳变沿为当前跳变沿的键值
*/
key_down = key_value & (key_old ^ key_value);
/*
第三行代码就是为了得到上一次的电平状态
*/
key_old = key_value;

假设按下的是B2按键

key_old key_value 对应的按键过程 key_old ^ key_value key_down key_up
0 0 未按下 0 0 0
0 2 按下过程中 2(0010) 2 0
2 2 按下稳定期间 0 0 0
2 0 抬起过程中 2(0010) 0 2(0010)

最后的运算结果Key_down只有在按键按下的过程中为按键值,持续时间大约10ms,key_up 只有在抬起过程中为按键值,其余时为0

下面代码长按时是一直触发的,所以可以实现长按某变量实现递增效果,把 else{} 改成 else if(10 == KEY_Time_Count) 的话长按就只执行1次多点

不要直接把按键扫描函数放定时器回调函数里,要设置一个Flag标志位,多少毫秒刷新一次即可【这个可以解决LCD在变化时按键不会有Bug】

/*************KEY.c***************/
uint8_t KEY_Time_Count;	//长短按计数
uint8_t key_value,key_up,key_down;


unsigned char KEY_Scan(void)  //带有返回值的按键扫描函数
{
	unsigned char ucKey_value;	//定义一个按键值的变量
	
	if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) == GPIO_PIN_RESET)	//按键B1按下键值返回为1
		ucKey_value=1;
	
	else if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) == GPIO_PIN_RESET)	//按键B2按下键值返回为2
		ucKey_value=2;
	
	else if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == GPIO_PIN_RESET)	//按键B3按下键值返回为3
		ucKey_value=3;
	
	else if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET)	//按键B4按下键值返回为4
		ucKey_value=4;
	
	else
		ucKey_value=0;	//无按键按下键值返回为0
	
	return ucKey_value;
}	
void KEY_Proc(void)
{
    static uint8_t key_old;
    
	key_value = KEY_Scan();//读取按键的键值
	key_up = ~key_value & (key_old ^ key_value);	//按键的上升沿检测 只在按键抬起的瞬间有效 其他时刻都为零无效
	
	key_down = key_value & (key_old ^ key_value);	//按键的下降沿检测 只在按键按下的瞬间有效 其他时刻都为零无效
	key_old = key_value;	//记录上一次按键按下后的键值
	
	if(key_down)	//当有按键按下时
	{
		KEY_Time_Count = 0; //将计时器清零 从零开始计时 此处使用了基础定时器用于计时
	}
	
	if(KEY_Time_Count < 10) //如果计时时间小于1s 短按
	{
		switch(key_up) //判断按键是否抬起 选择键值执行短按的相应程序
		{
			case 1:
				light1357_open(1);;
				break;
			case 2:
				light2468_open(1);
				//添加按键功能
				break;

			case 3:
				//添加按键功能
				break;

			case 4:
				//添加按键功能
				break;
		}
	}
	else //长按 计时时间超过1s
	{
		switch(key_value) //判断按键是否按下 选择按键的键值执行相应功能,如果这里改成key_up则按下松开才会执行下面
		{
			case 1:
				 light1357_open(0);
				break;
				
			case 2:
				light2468_open(0);
				//添加按键功能
				break;	
				
			case 3:
				//添加按键功能
				break;		

			case 4:
				//添加按键功能
				break;	
		}
		
	}
}
/*********TIMER.c***********/
extern uint8_t KEY_Time_Count;
uint8_t Key_flag = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	static uint16_t count_100ms = 0;
    static uint16_t count_10ms = 0;
	
	if(htim == &htim2)	//如果是TIM2产生中断
	{	
		count_100ms++;
        count_10ms++;
        if(10 == count_10ms)	//10ms扫描一次按键
        {
            count_10ms = 0;
            Key_flag = 1;
        }
		if(100 == count_100ms)	//100ms KEY_Time_Count加1
		{
			count = 0;
			KEY_Time_Count++;
		}
	}
}
/*********main.c***********/
int main()
{
    while(1)
    {
       if(Key_flag)
	   {
		  Key_flag = 0;
		  KEY_Proc();
	   }
    }
}

LCD

  • LCD驱动比赛会给,不需要CubeMX配置( lcd.h + lcd.c + fonts.h),这个跟旧板的LCD驱动是一模一样的
  • 介绍几个常用的函数
函数名 参数 功能
void LCD_Init(void) / LCD初始化(很重要,使用LCD前必须加)
void LCD_SetTextColor(vu16 Color) 颜色(一共10种) 文本颜色函数(即字体颜色)
但是作用效果仅对后面的显示有效
void LCD_SetBackColor(vu16 Color) 同上 背景颜色函数(可以是字体的背景色)
但是作用效果仅对后面的显示有效
void LCD_Clear(vu16 Color) 同上 清除全屏函数清屏之后屏幕颜色由参数决定
void LCD_DisplayStringLine(u8 Line, u8 *ptr) Line:表示数据放第几行,一个有10行
ptr:在LCD显示的数据,仅支持字符串
在LCD固定位置显示字串符数据(常跟 snprintf配合使用显示实时变化的数据)
LCD_ClearLine(u8 Line); Line:表示数据放第几行,一个有10行 清除某一行的内容
void LCD_DisplayChar(u8 Line, u16 Column, u8 Ascii); Line:表示数据放第几行,一个有10行
Column:表示列,一共20列
Ascii:表示要显示的字符
在指定位置显示一个字符
  • 避免LCD与LED的冲突
/*找到这3个函数
LCD_WriteReg()
LCD_WriteRAM_Prepare()
LCD_WriteRAM()
*/

//函数第一行加上
unsigned short PCOUT=GPIOC->ODR;
//在最后一行加上,用于锁存,否则导致操作LCD时LED不受控制
GPIOC->ODR=PCOUT;
  • 液晶屏尺寸是2.4寸,分辨率是 240 x 320(高x宽),分成 10行,20列,所以一个字符的大小是 24 x 16(每一行差值是24,每一列差值是16),实际上表示一列的时候是反着来的,319表示第一列,319-16表示第二列,那么 319-(16 * i)表示第i列

  • LCD_DisplayChar显示的是ASCII字符,如果要显示数字的话只能显示 0~9

  • 常用格式

特定格式 含义
%02d 自动添0
%.2f 指定小数位
%-d 左对齐
%% 百分号
%# o 带先导八进制(相当于数字前面有个0)
%o 不带先导八进制
%# x 带先导十六进制
%x 不带先导十六进制
  • 使用按键进行界面切换时需要切换时进行清屏 LCD_Clear(White);
  • 需要注意关注刷新LCD屏是否会造成系统的阻塞,有可能定时器10ms进行一次按键扫描但是因为LCD的刷新频繁导致30ms才能进行一次按键扫描
  • 比如在进行时间设置切换时,高亮来显示设置的是时还是分还是秒,通常定义一个 SettingModel来选择哪一个变量
  • 数组初始化时最好赋值 \0,避免后面有Bug
switch(SettingModel)
{
    case HOUR_SELECT:
        LCD_SetTextColor(red);	//设置高亮字体为红色
        LCD_DisplayChar(Line3,319-16*8,hour/10+48);	//加48显示的是数字不然显示的是字符
        LCD_DisplayChar(Line3,319-16*9,hour%10+48);
        LCD_SetTextColor(BLACK);	//默认颜色为黑色
        LCD_DisplayChar(Line3,319-16*11,min/10+48);	
        LCD_DisplayChar(Line3,319-16*12,min%10+48);
        LCD_DisplayChar(Line3,319-16*14,sec/10+48);
        LCD_DisplayChar(Line3,319-16*15,sec%10+48);
        break;
    case MIN_SELECT:
        ....
        break;
    case SEC_SELECT:
        ...
        break;
}

程序编写

/*************************MY_LCD.h*************************/
# ifndef __MY_LCD_H
# define __MY_LCD_H
# include "main.h"


void MY_LCD_Dis(void);
void hight_string(uint8_t* str,uint8_t Line,const uint8_t* pos,uint8_t pos_len,uint16_t color,uint16_t hight_color);
void MY_LCD_DisNum(uint32_t num);
void MY_LCD_Arr_Completion(uint8_t *ptr);
# endif
/*************************MY_LCD.h*************************/
# include "MY_LCD.h"
# include "lcd.h"
# include <stdio.h>
# include <string.h>
//# include "fonts.h"	//不需要包含这个头文件否则报错

uint8_t arr1[20]="HELLO World         ";	//第一行显示的字符串
uint8_t pos[5]= {1,4,7,8,9};	//需要高亮的索引,个数不能超过函数里定义的,如果需要则要在函数多添加几个元素即可(函数里只能多不能少)

void MY_LCD_Dis(void)
{
    LCD_Clear(White);
    LCD_SetBackColor(White);
	LCD_SetTextColor(Blue2);
    LCD_DisplayStringLine(Line0,(unsigned char*)arr1);	//第1行显示字串符
    hight_string(arr1,Line1,pos,5,White,Red);
    LCD_DisplayChar(Line2,319-(16*1),'@');	//在第2行第2列显示字符@
}

//某行显示字串符且高亮多个字符
/*
参数1:要显示的字符串
参数2:在哪一行(1~20)
参数3:在高亮哪几列(320~1)
参数4:需要高亮的数量
参数5:默认的背景颜色
参数6:需要高亮的颜色
*/
void hight_string(uint8_t* str,uint8_t Line,const uint8_t* pos,uint8_t pos_len,uint16_t color,uint16_t hight_color)
{
    uint8_t i,k;
	
	//下面这个效率高点不会高亮时闪屏,原因可能是减少for循环的次数提高执行效率,for循环只需执行19次但是需要手动设置需要高亮的索引
	# if 1
	for(i=0;i<19;i++)
	{
		if((i!=pos[0]) && (i!=pos[1]) && (i!=pos[2]) && (i!=pos[3]) && (i!=pos[4]))
		{
			LCD_DisplayChar(Line,319-(16*i),str[i]);	//如果不是需要高亮的字符则默认显示 
		}
	}
	for(k=0;k<pos_len;k++)
	{
		LCD_SetBackColor(hight_color);	//需要高亮的颜色
		LCD_DisplayChar(Line,319-(16*pos[k]),str[pos[k]]);	//要高亮的字符
	}
	LCD_SetBackColor(color);	//不需要高亮时的显示颜色
	# endif
}
//更改某几个字体颜色,代码跟上面一样只是把 "LCD_SetBackColor"变成"LCD_SetTextColor"

//显示变量
void MY_LCD_DisNum(uint32_t num)
{
	uint8_t DisNum_arr[20] = "";	//要初始化为0不然补齐空格函数可能补不干净
	uint8_t DisNum_pos[20] = {13,14,15,16,17};	//需要高亮的索引
	uint32_t dis_num = num;
	sprintf((char*)DisNum_arr,"TIM_NVIC_num:%d ",dis_num); //需要在%d后面加一个空格否则有阴影
	MY_LCD_Arr_Completion(DisNum_arr);	//补齐空格否则会出现阴影
	hight_string(DisNum_arr,Line3,DisNum_pos,5,White,Yellow);
}

//数组补齐空格
void MY_LCD_Arr_Completion(uint8_t *ptr)
{
    uint8_t i = 0;
    while(i<20)
    {
        if(0 == ptr[i])
        {
			ptr[i]=' ';
        }
        i++;
    }
}
  • 如果直接在定时器里进行变量显示会错乱而且会影响到按键,所以最好的方法就是设置一个标志位来进行LCD的刷新,同时注意刷新间隔不要跟其他外设定时间隔冲突,如果还有闪烁则再调整一下刷新频率【测试正常不会影响按键】
/*************************TIMER.c*************************/

uint8_t LCD_scan_Flag = 0;	//LCD刷新标志位

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	static uint16_t count_50ms = 0;
	
	if(htim == &htim2)	//如果是TIM2产生中断
	{	
		count_15ms++;

		if(50 == count_50ms)
		{
			count_50ms = 0;
			LCD_scan_Flag = 1;
		}
	}
}
/*************************main.c*************************/
int main(void)
{
  LCD_Init();	//LCD初始化
  MY_LCD_Dis();	//显示
    
    while(1)
    {
	  if(LCD_scan_Flag)
	  {
		  LCD_scan_Flag = 0;
		  MY_LCD_DisNum(99);	//显示变量
	  }
    }
}
  • 显示串口的数据也是这样需要判断 \n ,然后还要 减1 因为 \n 的前一个字节是 \r 也是不要的字节,中断标志位判断完就清除行再显示数据最后要把数组 memset 不然有阴影即可
/****************例如*********************/

if(RX_flag)
{
	RX_flag = 0;	//串口标志位
	LCD_ClearLine(Line4);	//清除行
	MY_LCD_Arr_Completion(USART1_RX_BUF);	//补齐空格否则会出现阴影
	LCD_DisplayStringLine(Line4,(uint8_t*)USART1_RX_BUF);	//重新显示
	memset(USART1_RX_BUF,0,sizeof(USART1_RX_BUF));		//初始化数组为0
}

实验现象

LCD翻转

  • 由文档 液晶控制器ILI9325 可知 LCD的输出方向由SS和GS这两位控制
  • SS:选择源驱动输出的移位方向(即控制上下)
  • GS:在SCN[4:0]和NL[4:0]确定的范围内,设置门控驱动器的扫描方向(即控制左右)

  • SS是R01h寄存器

  • GS是R60h寄存器(对应十进制为R96)

当SS = 0时,输出的移位方向从S1至S720(简单理解为输出方向从上往下)

当SS = 1时,输出的移位方向从S720至S1(简单理解为输出方向从下往上)

注意:蓝桥杯板子默认是从上往下

当GS = 0时,扫描方向为G1 ~ G320。(简单理解为从左往右)

当GS = 1时,扫描方向为G320到G1。(简单理解为从右往左)

注意:蓝桥杯板子默认是从左往右

修改 void REG_932X_Init(void)函数里的R1值即可

//原
LCD_WriteReg(R1  , 0x0000);	//从上往下
//改为
LCD_WriteReg(R1  , 0x0100);	//从下往上

//原
LCD_WriteReg(R96 , 0x2700);//从左往右
//改为
LCD_WriteReg(R96 , 0xA700);	//从右往左

LCD显示中文

  • 首先使用取模软件生成字模数组,阴码表示不亮的地方是0,相当于程序的背景,选择 行列式低位在前,显示完八个点之后换行,字体像素是 24*24

  • 直接复制改写自带的 LCD_DrawMonoPict函数即可
//汉字显示函数
/*
参数1:x坐标(0~240)
参数2:y坐标(319~0) 
参数3:背景颜色 
参数4:字体颜色 
参数5:需要显示的字模数组
*/
void LCD_DrawMonoPict1(uint16_t Xpos,uint16_t Ypos,uint32_t BackColor,uint32_t TextColor,uint8_t *Pict)
{
	uint8_t index = 0, i = 0,Xpos_Temp;
	Xpos_Temp = Xpos;	//保存初始坐标
	Ypos = 319 - (24*Ypos);	//右移多少个汉字再显示24*24一行最多显示13个
	LCD_SetCursor(Xpos, Ypos); 	//写入坐标
	LCD_WriteRAM_Prepare(); /* Prepare to write GRAM */
	
	for(index = 0; index < 72; index++)
	{
		//需要改变Y两次(换行),0-24  24-48  48-72
		if(24 == index || 48 == index)
		{
			Xpos = Xpos_Temp;	//x坐标恢复初始
			Ypos = Ypos - 8;	//Y右移8个像素
		}	
		for(i = 0; i < 8; i++)
		{
			if((Pict[index] & (1 << i)) == 0x00)	//如果对应位置数据是0x00则显示背景颜色
			{
				LCD_WriteRAM(BackColor);
			}
			else	//不为0显示字体
			{
				LCD_WriteRAM(TextColor);
			}
		}
		LCD_SetCursor(Xpos++, Ypos); 	//写入坐标
		LCD_WriteRAM_Prepare(); /* Prepare to write GRAM */
	}
}

高亮时分秒

在第6届那有高亮时分秒的要求,可以这样做:

一般通过按键来切换高亮部分,可以设置一个高亮标志位 Data.TIMER_CHOOSE_Flag(它有4种情况分别是 0无高亮 1高亮时 2高亮分 3高亮秒) ,按键每按一次改变这个标志位来达到切换效果 (可以通过switch实现),然后把高亮函数放在switch结束下面,把高亮标志位跟存储时分秒的结构体传进去就OK了

函数里就是通过改变对应字符位置的背景颜色字体颜色,需要注意LCD_DisplayChar函数参数是字符,所以整型变量需要 +'0' 来转换成字符型显示,行的位置就是刷新标志位*24,列的位置就是第几个字符*16(从左到右是320开始的,第几个是0~19一共20个可显示字符),还原的话: 高亮时的前一次高亮是秒,所以还原秒, 高亮分 的前一次高亮是时,所以还原时, 高亮秒 的前一次高亮是分,所以还原分;记得不要刷新LCD标志位否则高亮效果全没了

//高亮时分秒函数
//参数1:高亮的标志位
//参数3:存储时/分/秒的结构体
void HightLight(uint8_t Index,PARAMETER_TypeDef *Sparameter)
{
	LCD_SetBackColor(White);	//高亮前颜色
	LCD_SetTextColor(Black);
	switch(Index)
	{
		case 1:
		{
			LCD_DisplayChar(C3_STATE*24,319-(6*16),Sparameter->temp_hour/10+'0');
			LCD_DisplayChar(C3_STATE*24,319-(7*16),Sparameter->temp_hour%10+'0');
			break;
		}
		case 2:
		{
			LCD_DisplayChar(C3_STATE*24,319-(9*16),Sparameter->temp_min/10+'0');
			LCD_DisplayChar(C3_STATE*24,319-(10*16),Sparameter->temp_min%10+'0');			
			break;
		}
		case 3:
		{
			LCD_DisplayChar(C3_STATE*24,319-(12*16),Sparameter->temp_sec/10+'0');
			LCD_DisplayChar(C3_STATE*24,319-(13*16),Sparameter->temp_sec%10+'0');			
			break;
		}
		default:break;		
	}
	LCD_SetBackColor(Black);	//还原颜色
	LCD_SetTextColor(White);
	switch(Index)
	{
		case 1:
		{
			LCD_DisplayChar(C3_STATE*24,319-(12*16),Sparameter->temp_sec/10+'0');
			LCD_DisplayChar(C3_STATE*24,319-(13*16),Sparameter->temp_sec%10+'0');			

			break;
		}
		case 2:
		{
			LCD_DisplayChar(C3_STATE*24,319-(6*16),Sparameter->temp_hour/10+'0');
			LCD_DisplayChar(C3_STATE*24,319-(7*16),Sparameter->temp_hour%10+'0');						
			break;
		}
		case 3:
		{
			LCD_DisplayChar(C3_STATE*24,319-(9*16),Sparameter->temp_min/10+'0');
			LCD_DisplayChar(C3_STATE*24,319-(10*16),Sparameter->temp_min%10+'0');			
			break;
		}
		default:break;		
	}	
}

定时器

查看手册可以看到定时器挂载在哪个时钟上

定时器的时钟看 APB1 或者 APB2,还要看是否有分频

公式:

定时器定时时间(单位:s)=(arr+1)(psc+1)时钟频率=(arr+1)(psc+1)80000000Hz\text{定时器定时时间(单位:s)}=\frac{(arr+1)(psc+1)}{\text{时钟频率}}=\frac{(arr+1)(psc+1)}{80000000Hz}

  • 定时器在CubeMX配置完后需要在main函数里开启
//使能定时器x
HAL_TIM_Base_Start(&htimx);	
//如果需要定时器中断则
HAL_TIM_Base_Start_IT(&htimx);	
  • 需要注意,HAL库的所有定时器中断都会调用同一个回调函数,在函数里面进行判断是哪个定时器中断产生了中断则进行对应的处理
  • stm32g4xx_ hal _tim.c 里面有弱函数定义,我们只需把函数直接复制出来自己的文件里即可,把前面的__weak去掉
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
     if(htim == &htimx)	//判断是哪个定时器产生中断
     {
         ...
     }
    ...
}
  • 关于配置时是否使能 auto-reload preload (自动重装载)

这里的自动重装载跟学51的不一样(51的自动重装载定时器的值是为了保证下一次计数值溢出时重新装载计数值产生定时器中断),这里自动重装载寄存器是 影子寄存器,如果不使能自动重装载,这次修改的值,立马会被执行。而使能自动重装载,这次修改值会等到这次执行完后,才去执行(目的是为了保证自动重装载寄存器在合适的时候被修改,不允许其随便被修改,否则可能导致在切换的时候发生事与愿违的结果); 所以如果对通道时序同步不严谨或者不需要改计数值则可以不使能,如果是多通道或者频繁改计数值建议使能

简单来说就是两句话:

auto-reload precload=Disable:自动重装载寄存器写入新值后,计数器立即产生计数溢出,然后开始新的计数周期

auto-reload precload=Enable:自动重装载寄存器写入新值后,计数器完成当前旧的计数后,再开始新的计数周期

滴答定时器

SysTick系统滴答定时器位于 Cortex M4内核 中。 HAL_Delay() 函数,此函数利用的就是SysTick系统滴答定时器,Systick是一个 24位 的定时器,一次最多可以计数 2^24^(即16777216) 个时钟脉冲,这个脉冲计数值保存在当前计数值寄存器 STK_VAL, 只能向下计数,每接收到一个时钟脉冲,STK_VAL 的值就会向下减1,当减到0时,硬件会自动把重装载寄存器 STK_LOAD 中保存的数据加载到 STK_VAL,重新开始向下计数。如果 STK_VAL 的值被减至0时,会触发异常产生中断

寄存器

寄存器名称 描述
CTRL SysTick控制及状态寄存器
LOAD SysTick重装载数值寄存器
VAL SysTick当前数值寄存器
CALIB SysTick校准数值寄存器

Systick的脉冲频率从何而来呢?

跳转至 HAL_Init() 函数里可以看到这一句话:

/* Use SysTick as time base source and configure 1ms tick (default clock after Reset is HSI) */ 意思是: 使用 SysTick 作为时基源并配置 1ms 节拍(复位后默认时钟为 HSI)

在while(1)循环中,除了在执行我们编程的函数外,内核还会不断的产生1ms中断(只是我们看不见),一直循环,直到溢出达到2^32^(大约要一直运行40多天);中断函数在 stm32g4xx_hal.c

  • 在中断函数中 uwTick 会每1ms加1

  • uwTick 是一个非常重要的变量

  • uwTickFreq 的值为1(可跳转深入查看)

uwTick的作用

HAL_Delay() 中的 HAL_GetTick() 的值来自 uwTick

wait 的值为为用户输入的需要延时的值(单位ms)

tickstartHAL_GetTick() 最初的值(即进入中断时uwTick的值),固定不变

HAL_GetTick()tickstart 的差值若小于用户定义的值,则继续 循环等待;若超过值,则 跳出循环(即跳出HAL_Delay() ),执行下一个函数

程序编写

以下代码功能是实现 LED1 一秒闪烁

代码需要写在 stm32g4xx_it.c 里的 SysTick_Handler() 函数里

void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */
  static uint16_t num;
  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */
  num++;
  if(1000 == num)	//隔1s进入函数
  {
	num = 0;
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_8);	//翻转电平
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);	//拉高
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);	//拉低
  }	  
  /* USER CODE END SysTick_IRQn 1 */
}

USART

硬件连接

管脚 对应
PA9 TX
PA10 RX

CubeMX配置

一定要先配置引脚模式再激活串口1,不然可能默认管脚会错得手动改

程序编写

串口发送

重定向写法

  • 需要注意要在设置里勾选 Use MicroLlB
  • 换行是 \r\n(对应十六进制是 0x0D和0x0A)
/*************************USART1.c*************************/

//重定向发送函数
void PrintTest(void)
{
	HAL_Delay(1000);
	printf("测试\r\n");
	HAL_Delay(1000);
}


//重定向printf函数(&必须要写)
int fputc(int ch, FILE *f)
{
	HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,0xFFFF);
    return ch;
}

HAL库写法

  • 注意中文占 2个字节
  • 发送一般不需要中断,所以使用 HAL_UART_Transmit(轮询模式发送) 或 HAL_UART_Transmit_IT(中断模式发送) 或 HAL_UART_Transmit_DMA(DMA发送)都行
  • 数据长度通常使用 strlen 获取,也可以直接写数值
/*************************USART1.c*************************/

//HAL库发送函数写法1
void TXTest(void)
{
	HAL_UART_Transmit(&huart1,(uint8_t*)&"按键按下\r\n",10,0xFFFF);
}

//HAL库发送函数写法2
void TXTest(void)
{
	uint8_t arr[] = "按键按下\r\n";
	HAL_UART_Transmit(&huart1,(uint8_t*)&arr,strlen((char*)arr),0xFFFF);
}

//然后在按键执行函数里调用即可,实现按下按键发送一次数据
  • DMA发送需要配置一下:

注意:DMA初始化必须要在串口的上面【否则发送失败】

串口数据发送寄存器只能存储8bit,每次发送一个字节,所以数据长度选择Byte

//使用发送函数
uint8_t tx_buff[20]="I Love You,diu ni ma";

HAL_UART_Transmit_DMA(&huart1,tx_buff,20);

串口接收

  • 串口接收一般使用中断模式或DMA模式
  • 回调函数在 stm32g4xx_hal_uart.h

中断接收写法

/*************************USART1.h*************************/

//接收数组的最大字节
#define USART1_RX_LEN 100

extern char USART1_RX_BUF[USART1_RX_LEN];	//存放接收数据的最终数组
extern uint8_t RX_flag;
extern uint8_t USART1_NewData;	//最新接收的字节
/*************************USART1.c*************************/

char USART1_RX_TEMP[USART1_RX_LEN];	//存放接收数据临时数组
char USART1_RX_BUF[USART1_RX_LEN];	//存放接收数据的最终数组
uint8_t USART1_NewData;	//最新接收的字节
uint8_t USART1_Count = 0;	//计数值
uint8_t RX_flag;

//接收中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(&huart1 == huart)	//如果是串口1
	{	
		if('\n' == USART1_NewData)	//遇到换行
		{
			USART1_RX_TEMP[USART1_Count-1] = 0;	//把'\n'前面的'\r'去掉
			strcpy(USART1_RX_BUF,USART1_RX_TEMP);	//复制数组
			RX_flag = 1;
			USART1_Count = 0;
			memset(USART1_RX_TEMP,0,sizeof(USART1_RX_TEMP));	//初始化为0
		}
		else
		{
			USART1_RX_TEMP[USART1_Count++] = USART1_NewData;			
		}

		HAL_UART_Receive_IT(&huart1,(uint8_t*)&USART1_NewData,1);
	}
}
/*************************main.c*************************/

int main()
{
    HAL_UART_Receive_IT(&huart1,(uint8_t*)&USART1_NewData,1);	//先开启一次
    
    while(1)
    {
	  if(RX_flag)
	  {
		  RX_flag = 0;
		  printf("%s",USART1_RX_BUF);	//发送回串口助手显示
	  }        
    }
}

中断+库函数接收写法

  • 使用到 sscanf 库函数

:没定义其他变量跟上面一样

/*************************USART1.h*************************/

//存放接收数据结构体
typedef struct USART1_Receive{

	char c1[3];
	int x;
	int y;

}RX_Data;
extern RX_Data d1;
/*************************USART1.c*************************/

RX_Data d1;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(&huart1 == huart)	//如果是串口1
	{	
		if('\n' == USART1_NewData)	//遇到换行
		{
			USART1_RX_TEMP[USART1_Count-1] = '\0';	//把'\n'前面的'\r'去掉
			sscanf(USART1_RX_TEMP,"%3s:%d:%d",d1.c1,&d1.x,&d1.y);	//提取数据
			strcpy(USART1_RX_BUF,USART1_RX_TEMP);	//复制数组
			RX_flag = 1;
			USART1_Count = 0;
			memset(USART1_RX_BUF,0,sizeof(USART1_RX_BUF));	//初始化为0
			memset(USART1_RX_TEMP,0,sizeof(USART1_RX_TEMP));	//初始化为0
		}
		else
		{
			USART1_RX_TEMP[USART1_Count++] = USART1_NewData;			
		}

		HAL_UART_Receive_IT(&huart1,(uint8_t*)&USART1_NewData,1);
	}
}
/*************************MY_LCD.c*************************/

//显示变量
void MY_LCD_DisNum(uint32_t num)
{
	uint8_t TestArr[20];
	
	if(RX_flag)
	{
		RX_flag = 0;
		LCD_ClearLine(Line4);
		LCD_ClearLine(Line5);
		sprintf((char*)TestArr,"%d %d",d1.x,d1.y);
		LCD_DisplayStringLine(Line4,(uint8_t*)d1.c1);	
		LCD_DisplayStringLine(Line5,(uint8_t*)TestArr);
		memset(TestArr,0,sizeof(TestArr));		
	}
}
  • 如果是用资源包的串口助手需要注意要手动换行!

空闲中断+DMA接收写法

CubeMX配置:

当接收到1个字节,就会产生 RXNE 中断,当接收到一帧数据,就会产生 IDLE 中断。比如给单片机一次性发送了8个字节,就会产生8次RXNE中断,1次IDLE中断。

STM32的IDLE的 中断产生条件 :在串口无数据接收的情况下,不会产生,当清除IDLE标志位后,必须又接收到第一个数据后,才开始触发,一但接收的数据断流,没有接收到数据,即产生IDLE中断

把 【stm32g4xx_it.c】里的中断服务函数剪切到 【usart.c】里(不能复制不然存在两个会报错,而且每次生成CubeMX都需要去删除)

/****************usart.c(CubeMX生成的)*********************/

#define UART1_RX_LEN 64	//接收缓冲区最大字节

volatile uint8_t UART1_RX_BUF[UART1_RX_LEN];	//接收数据缓存数组
volatile uint8_t UART1_RX_DATA_LEN;	//接收数据的长度
uint8_t UART1_over=0;	//接收完成是否标志位

//在 【MX_USART1_UART_Init】 函数最后添加以下代码
void MX_USART1_UART_Init(void)
{
    ....
     
        
   __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);	//开启串口1接收中断
   __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);	//开启串口1空闲中断
   HAL_UART_Receive_DMA(&huart1,(uint8_t*)UART1_RX_BUF,UART1_RX_LEN);	//开启串口1 DMA接收        
}

//串口中断服务函数
void USART1_IRQHandler(void)
{
    if(SET == __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))	//判断是不是空闲中断
    {
        __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_IDLE);	//清除空闲中断标志位
        HAL_UART_DMAStop(&huart1);	//停止串口1 DMA接收
        UART1_RX_DATA_LEN = UART1_RX_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);	//最大个数-未传输的数据个数=接收的数据个数
        HAL_UART_Transmit_DMA(&huart1, (uint8_t *)UART1_RX_BUF, UART1_RX_DATA_LEN);	//发送回去
        UART1_over = 1;	//接收一帧完成标志位
    }
    HAL_UART_IRQHandler(&huart1);
}

uint8_t buf[20] = "";

//对接收数据进行处理
void UART1_function(void)
{
    uint8_t i = 0, j = 0;

    if(UART1_over)
    {
        UART1_over = 0;
        memset(buf, ' ', sizeof(buf));	//这里初始化为0没效果需要初始化空格
        if(UART1_RX_DATA_LEN > 15)
        {
            UART1_RX_DATA_LEN = 15;
        }

        for(i = 0; i < UART1_RX_DATA_LEN; i++)	
        {
            buf[i] = UART1_RX_BUF[i];
            if('\r' == buf[i] || '\n' == buf[i])//\r\n变成空格,变成\0则不会覆盖
            {
                buf[i] = ' ';
            }
        }
        sprintf((char *)uart1_scan, "UART:%s", buf);
        HAL_UART_Receive_DMA(&huart1, (uint8_t *)UART1_RX_BUF, UART1_RX_LEN);				//使能串口1 DMA接收
    }
}
/**********************MY_LCD***********************/


//界面2显示pwm相关
uint8_t uart1_scan[20] = "UART:               ";

void pwm_display(void)
{
    LCD_DisplayStringLine(Line7, (uint8_t *)uart1_scan);
}

串口接收判断时间

//示例
if(RX_FLAG)
{
    RX_FLAG = 0;
    if(RX_LEN > 1)
    {
        uint16_t hh, mm, ss, x, y;
        char arr2[5];

        if((RX_BUFF[2] == ':') && (RX_BUFF[5] == ':') && (RX_BUFF[8] == '-') && (RX_BUFF[9] == 'P'))
        {
            if((RX_BUFF[10] == 'A') && (RX_BUFF[12] == '-') && (RX_BUFF[14] == 'S'))
            {
                snprintf(arr2, sizeof(arr2), "%d%d", RX_BUFF[0] - '0', RX_BUFF[1] - '0');	//合并
                hh = atoi(arr2);	//字符串转整型
                if(hh >= 0 && hh <= 23)
                {
                    snprintf(arr2, sizeof(arr2), "%d%d", RX_BUFF[3] - '0', RX_BUFF[4] - '0');
                    mm = atoi(arr2);
                    if(mm >= 0 && mm <= 59)
                    {
                        snprintf(arr2, sizeof(arr2), "%d%d", RX_BUFF[6] - '0', RX_BUFF[7] - '0');
                        ss = atoi(arr2);
                        if(ss >= 0 && ss <= 59)
                        {
                            if(((RX_BUFF[13] - '0') >= 0) && ((RX_BUFF[13] - '0') < 10))
                            {
                                if(RX_BUFF[11] - '0' == 1)
                                {
                                    Parameter_Data.Run_pwm_state = 1;
                                    Parameter_Data.Pwm_time = RX_BUFF[13] - '0';
                                    snprintf(Parameter_Data.RX_ARR, sizeof(Parameter_Data.RX_ARR), "%02d:%02d:%02d-PA%d-%dS", hh, mm, ss, RX_BUFF[11] - '0', RX_BUFF[13] - '0');
                                    Alarm_set(hh, mm, ss);	//设置时间
                                }
                                else if(RX_BUFF[11] - '0' == 2)
                                {
                                    Parameter_Data.Run_pwm_state = 2;
                                    Parameter_Data.Pwm_time = RX_BUFF[13] - '0';
                                    snprintf(Parameter_Data.RX_ARR, sizeof(Parameter_Data.RX_ARR), "%02d:%02d:%02d-PA%d-%dS", hh, mm, ss, RX_BUFF[11] - '0', RX_BUFF[13] - '0');
                                    Alarm_set(hh, mm, ss);
                                }
                                else
                                {
                                    ;
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    HAL_UART_Receive_DMA(&huart1, (uint8_t *)RX_BUFF, 20);
}
}

ADC

  • G4板载了 四个电位器,其中左边两个是电压采集用的
  • HAL_ADC_STATE_REG_EOC 表示转换完成标志位,转换数据可用
名词 描述
量程 指ADC所能输入模拟信号的类型和电压范围,即参考电压。信号类型包括单极性和双极性,蓝桥杯嵌入式G431开发板的输入电压范围是 0~3.3V
转换位数 量化过程中的量化位数n。蓝桥杯嵌入式G431开发板我习惯使用的位数是 12位
分辨率 ADC能够分辨的模拟信号最小变化量
  • ADC电压值计算公式:

分辨率=量程2n\text{分辨率} = \frac{\text{量程}}{2^n}

最终显示电压值=分辨率×ADC采集值\text{最终显示电压值} = \text{分辨率} \times \text{ADC采集值}

V(ADC)=Value(ADC)×V(ref)4096V(ADC) = Value(ADC) \times \frac{V(ref)}{4096}

其中,stm32的ADC是 12位 的,所以AD值的最大值是 4096

V(ADC)为算出的电压值;

Value(ADC)为ADC采集值;

V(ref)为参考电压,一般为3.3V。

比如你STM32的参考电压为3.3v,采集的AD值为1024,那么转换为电压:

V(ADC)=1024×3.3/4096=0.825VV(ADC)=1024 \times 3.3/4096 =0.825V

硬件连接

管脚 对应
PB15 R37(ADC2通道15)
PB12 R38(ADC1通道11)

CubeMX配置

注意:以下配置为ADC直接采样的配置

选择对应的通道

ADC默认设置为 12位精度数据右对齐,转换时间可以看个人设置,转换时间越长精度越高rank表示通道的转换顺序,从小到大采集

程序编写

注意

只有 ADC1ADC3 可以产生DMA请求,ADC2虽然没有直接与DMA相连,但可以与ADC1配合,在 双ADC模式 下共同工作并使用DMA传输,提高工作效率。因此双模式主要针对ADC1和ADC2,二者既可以相互独立工作,也可以主从配合工作

ADC在使能后需要在main函数里先进行 校准 然后再开始转换,参数2要选择 单端模式跟CubeMX使能通道那保持一致

ADC直接采样

/*************************my_adc.c*************************/


//获取ADC的值函数
double getADC(ADC_HandleTypeDef *hadc)
{
    uint32_t adc;
    HAL_ADC_Start(hadc);
	HAL_ADC_PollForConversion(hadc,500);//等待采集结束,超时是500ms
	if(HAL_IS_BIT_SET(HAL_ADC_GetState(hadc), HAL_ADC_STATE_REG_EOC))//读取ADC完成标志位
	{
		adc = HAL_ADC_GetValue(hadc);
		return adc * 3.3 / 4096;		
	}
	
	return 0;	//错误返回0
}
/*************************MY_LCD.c*************************/

//显示变量
void MY_LCD_DisNum(uint32_t num)
{
	uint8_t ADC1_Num[20] = "";	//ADC1
	uint8_t ADC2_Num[20] = "";	//ADC2

	sprintf((char*)ADC1_Num,"V1:%.2f",getADC(&hadc1));
	MY_LCD_Arr_Completion((uint8_t*)ADC1_Num);
	LCD_DisplayStringLine(Line5,(uint8_t*)ADC1_Num);
	
	sprintf((char*)ADC2_Num,"V2:%.2f",getADC(&hadc2));
	MY_LCD_Arr_Completion((uint8_t*)ADC2_Num);
	LCD_DisplayStringLine(Line6,(uint8_t*)ADC2_Num);
}

需要放在定时器里使用标志位进行更新,一直转换的话按键会出现Bug【如果ADC值显示一直在变就会导致按键出现Bug不变就没事】

/**************TIMER.c************/

uint8_t ADC_flag = 0;	//ADC转换标志位

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	static uint16_t count_80ms = 0;	
	
	if(htim == &htim2)	//如果是TIM2产生中断
	{	
		count_80ms++;

		if(80 == count_80ms)
		{
			count_80ms = 0;
			ADC_flag = 1;
		}
	}
}
/**************main.c************/
int main()
{
    HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);    //ADC1校准
  	HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);    //ADC2校准 
    
    
    while(1)
    {
        ...
    }
}

ADC+DMA

  • 预分频是控制频率的,可以选择 同步 或者 异步,好像说 ADC时钟最好不要超过36Mhz,但是按默认也没看到有Bug,实在有Bug就选 同步4分频(默认是同步2分频)
  • 不要开启ADC中断
  • 注意初始化时序,必须是 DMA初始化在前,ADC初始化在后
  • 在LCD上显示抖动的话可以适当的把转换间隔加大点【MX配置那转换时间选最大那个】
  • 中位值滤波法(冒泡排序然后去掉最大最小值再求平均数) 可以滤除偶然因素引起的脉冲干扰,适用于变化缓慢的采样系统,如温度、液位、流量等系统的测量【实在记不住可以使用平均值滤波法】
名词 描述
Scan Conversion Mode【扫描模式】 ADCx每次转换完一个通道,会继续去到下一个通道进行采样转换,开启需要配合DMA进行数据存储不然数据会覆盖上一次
Mode【模式】 常见的有:
Independent mode --独立模式
Dual regular simultaneous mode only --ADC双重模式:例如ADC1_IN0,ADC2_IN0在同一管脚上,采集的是同一管脚上的电压
Dual interleaved mode only --ADC三重模式:例如ADC1_IN0,ADC2_IN0,ADC3_IN0在同一管脚上,采集的是同一管脚上的电压
Continuous Conversion Mode【连续转换模式】 使能后通道会进行连续的转换,在一轮转换后(即所有通道转换完一遍),又从第一个通道重新开始下一轮
External Trigger Conversion Source【触发方式】 定时器/外部中断/软件
DMA_Mode【DMA模式】 有两种:
Normal:每次使能仅进行一轮传输
Circular:循环

CubeMX配置:

ADC1ADC2 配置一样,记住选择word然后程序里数组定义也要用uint32_t,选择halfword就定义uint16_t一定要对应,否则会出现很大的数值溢出

/**************main.c************/

uint32_t ADC2_Value[30];	//一个通道转换30次
uint32_t ADC1_Value[30];
uint32_t ADC_1 = 0;	//求平均值后的值
uint32_t ADC_2 = 0;

int main()
{
    HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);    //ADC1校准
  	HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);    //ADC2校准 
    HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&ADC1_Value,30);	//启动ADC_DMA
    HAL_ADC_Start_DMA(&hadc2,(uint32_t*)&ADC2_Value,30);   //启动ADC_DMA
    
    while(1)
    {
        ...
    }
}
/**************main.h************/

extern uint32_t ADC2_Value[30];
extern uint32_t ADC1_Value[30];
extern uint32_t ADC_1;
extern uint32_t ADC_2;
/**************my_adc.c************/

//平均值滤波
void ADC_DMA_Test(void)
{
    ADC_2 = 0;
    for(uint8_t i = 0; i < 30; i++)
    {
        ADC_2 += ADC2_Value[i];
    }
    ADC_1 = 0;
    for(uint8_t i = 0; i < 30; i++)
    {
        ADC_1 += ADC1_Value[i];
    }
}

//ADC中位值滤波【ADC转换10个值】用到冒泡排序
//一般放在时间片轮询里100ms采集一次
float Adc_Proc(uint32_t _buf[])
{
    uint8_t i, j,count;
    float Adc_Val = 0;
    float temp;
    HAL_ADC_Stop_DMA(&hadc2);	//停止ADC2 DMA
    for(i = 0; i < 10 - 1; i++)	//-1是因为不用跟自己比
    {
		count = 0;
        for(j = 0; j < 10 - 1 - i; j++)	//size-1-i是因为每一趟就会少一个数比较
        {
            if (_buf[j] < _buf[j + 1])	//降序排序
            {
                temp = _buf[j];
                _buf[j] = _buf[j + 1];
                _buf[j + 1] = temp;
				count = 1;
            }
        }
		if(0 == count)	//如果某一趟没有交换位置,则说明已经排好序,直接退出循环
		{
			break;
		}
    }
	//去掉最小和最大值然后取平均值
    for(i = 1; i < 10 - 1; i++)
	{
        Adc_Val += _buf[i];		
	}
	HAL_ADC_Start_DMA(&hadc2,(uint32_t *)ADC2_ch15_buf,ADC2_ch15_len);	//重新使能ADC DMA
    return (Adc_Val / 8) / 4096 * 3.3f;
}
/**************MY_LCD.c************/

//显示变量
void MY_LCD_DisNum(uint32_t num)
{
    uint8_t ADC1_Num[20] = "";	//ADC1
    uint8_t ADC2_Num[20] = "";	//ADC2

	if(ADC_flag)
	{
		ADC_flag = 0;
		ADC_DMA_Test();	//平均值滤波
        sprintf((char *)ADC1_Num, "V1:%.2f", (double)ADC_1/30*3.3/4096);
        MY_LCD_Arr_Completion((uint8_t *)ADC1_Num);
        LCD_DisplayStringLine(Line5, (uint8_t *)ADC1_Num);

        sprintf((char *)ADC2_Num, "V2:%.2f", (double)ADC_2/30*3.3/4096);
        MY_LCD_Arr_Completion((uint8_t *)ADC2_Num);
        LCD_DisplayStringLine(Line6, (uint8_t *)ADC2_Num);
	}
}
/**************TIMER.c************/

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	static uint16_t count_80ms = 0;	
	
	if(htim == &htim2)	//如果是TIM2产生中断
	{	
		count_80ms++;

		if(80 == count_80ms)
		{
			count_80ms = 0;
			ADC_flag = 1;
		}
	}
}

注意

  • 如果是同1个ADC的不同通道同时采集则需要配置DMA和勾选对应通道,打开 扫描模式,连续模式,DMA请求,改通道数为2 ,然后程序上定义一个大小为2的数组即可
  • 如果出现显示0,检查一下是否有采集到数据,如果有则检查一下是否显示时没在前面加 (double) 类型转换

DAC

  • 板子扩展接口部分找到DAC的两个管脚

  • 4 个12位DAC通道(2个外部缓冲和2个内部非缓冲)可以用于 将数字信号转换为模拟电压信号输出

  • 噪声波的产生,三角波产生,锯齿波产生

  • DAC输出电压计算公式,可以发现公式跟ADC是相反的(结果是uint16_t类型):

V(DAC)=Vol×4096/3.3fV(DAC) = Vol \times 4096/3.3f

然后再通过 ADC 公式就可以算出来具体的电压值

硬件连接

管脚 对应通道
PA4 DAC1_OUT1
PA5 DAC1_OUT2

CubeMX配置

名词 描述
Connected to external pin only 只连接到外部引脚
Connected to external pin and to on chip-peripherals 连接到外部引脚和片上外设
oUT1 Connected to on chip-peripherals only 仅连接到片上外设

程序编写

/*************************main.c*************************/

int main()
{
  HAL_DAC_Start(&hdac1,DAC_CHANNEL_1);	//开启DAC通道1
  HAL_DAC_Start(&hdac1,DAC_CHANNEL_2);  //开启DAC通道2
  set_DAC_value(&hdac1,DAC_CHANNEL_1,2.2);	//设置模数转换后的值为2.2v ---2.2*4096/3.3 = 2730
  set_DAC_value(&hdac1,DAC_CHANNEL_2,3.1);  //设置模数转换后的值为3.1v
    
    while(1)
    {
        ...
    }
}
/*************************my_adc.c*************************/

//设置DAC值
void set_DAC_value(DAC_HandleTypeDef *hdac,uint32_t Channel,float Vol)
{
	uint16_t temp;
	temp = Vol*4096/3.3f;
	HAL_DAC_SetValue(hdac,Channel,DAC_ALIGN_12B_R,temp);	//设置DAC输出值
}
/*************************MY_LCD.c*************************/

//显示变量
void MY_LCD_DisNum(uint32_t num)
{
	uint8_t DAC_Num[20] = "";	//DAC通道1

	if(ADC_flag)
	{
		ADC_flag = 0;

	sprintf((char*)DAC_Num,"DAC_value:%d",HAL_DAC_GetValue(&hdac1,DAC_CHANNEL_1));//获取DAC的值
		MY_LCD_Arr_Completion((uint8_t *)DAC_Num);
		LCD_DisplayStringLine(Line7,(uint8_t *)DAC_Num);
	}
}

烧录程序进开发板后可用电压表测量PA4/PA5管脚电压,也可以使用杜邦线把PA4和PA5短接,一个使用DAC功能,另一个使用ADC功能去读取然后显示

DAC输出锯齿波

CubeMX配置跟上面一样

/*************************main.c*************************/
//方案1
int dacvalue;

int main()
{
    wwhile(1)
    {
        for(dacvalue = 0;dacvalue <= 20;dacvalue++)//循环次数和波的阶梯化相关,循环次数越多波越光滑
        {
            HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12_R,124*dacValue);//最高2480/4096*3.3=1V
            if(HAL_DAC_Start(&hdac,DAC_CHANNEL_1)!=HAL_OK)	//开启DAC通道1
            {
                Error_Handler();
            }
            HAL_Delay(1);	//频率和延时时间以及阶梯数有关
        }
    }
}
//方案2
void generate_waveform(void)
{
	//乘以124
    uint16_t dacvalues[] = {0, 124, 248, 372, 496, 620, 744, 868, 992, 1116, 1240, 1364, 1488, 1612, 1736, 1860, 1984, 2108, 2232, 2356, 2480};
    uint32_t delaycount = 100000; // 延迟计数器,用来控制输出波形的频率

    // 判断 DAC 是否处于空闲状态
    if(HAL_DAC_GetState(&hdac1) == HAL_DAC_STATE_READY)
    {
        for(int i = 0; i < sizeof(dacvalues)/sizeof(dacvalues[0]); i++)
        {
            // 设置 DAC 输出值
            HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dacvalues[i]);

            // 启动 DAC 通道 1 的输出
            if(HAL_DAC_Start(&hdac1, DAC_CHANNEL_1) != HAL_OK)
            {
                Error_Handler();
            }

            // 等待延时结束
            while(delaycount--)
            {
                __asm("nop"); // 执行空指令,用来增加延时时间
            }
            delaycount = 100000; // 延迟计数器复位
        }
    }
}

实验现象

DAC输出三角波

三角波的数据生成是由一个单独的计数器往复加减生成的, 每次触发之后该计数器会加1/减1。在每个周期过峰值之前,每次触发条件之后 加1 ;过峰值之后,每次触发条件之后 减1;要先想生成三角波必须以 固定的周期加减其输出幅度(配合定时器)

三角波的幅度可以设定为 1~4095(即0~3.3v),也就是三角波计数器峰值的大小

比如现在三角波幅度设定为4095,需要生成一个 1Hz 的三角波,则周期就等于 1s,半个周期就是500ms,则

触发周期=500ms40960.12207ms122us\text{触发周期} = \frac{500ms}{4096} \approx 0.12207ms \approx 122us

故定时器的频率就是设置成 122us,则 arrpsc 值可以通过计算:

Tout(溢出时间)=(Arr+1)(PSC+1)TclkTout\text{(溢出时间)} = \frac{(Arr+1)(PSC+1)}{Tclk}

0.000122s=x80000000得出x=9760当PSC取(80-1)时,Arr=(122-1)0.000122s = \frac{x}{80000000} \text{得出}x= 9760 \text{当PSC取(80-1)时,Arr=(122-1)}

CubeMX配置:

以DAC通道1为例,定时器也可以用其他的(选项上有的定时器)

/*************************main.c*************************/

int main()
{
    HAL_TIM_Base_Start(&htim6);	//开启定时器6
    HAL_DAC_Start(&hdac1,DAC_CHANNEL_1);	//开启DAC通道1
    
    while(1)
    {
        ...
    }
    
}

实验现象

蜂鸣器(已取消)

注意:蓝桥杯官方在新板G4上已经取消了蜂鸣器这个外设【大科电子的板还有可以玩玩】,可能原因是赛场上太吵了吧

硬件连接

管脚 对应 注意
PB3 BUZZER 低电平有效【PNP】

CubeMX配置

默认设为 高电平,表示蜂鸣器上电时处于关闭状态

程序编写

/*************************LED.c*************************/

//蜂鸣器【参数:1表示打开蜂鸣器 0表示关闭】
void Buzzer_test(uint8_t swch)
{
	if(SET == swch)
	{
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_3,GPIO_PIN_RESET);	//打开蜂鸣器
	}
	else if(RESET == swch)
	{
		HAL_GPIO_WritePin(GPIOB,GPIO_PIN_3,GPIO_PIN_SET);	//关闭蜂鸣器		
	}
}

使用时直接调用即可,蜂鸣器响了会一直在响除非你关闭它才关闭

I2C之EEPROM

需要把 ii2.hal.c 里的 I2CWaitAck() 函数里的 SDA_Output_Mode(); 放到 return

I2CInit() 函数中没有时钟使能,因为 MX_GPIO_Init() 中有,所以记得放在它的下面才能初始化成功

24C02是 2Kbits,一共 256 字节,8 个字节一个页,32页,所以可寻址范围是 0x00~0xFF

1kbits=1024bits=10248Bytes=128Bytes(字节)1\text{kbits} = 1024\text{bits} = \frac{1024}{8}\text{Bytes} = 128\text{Bytes(字节)}

每页的首地址 = 页数*每页多少字节+x(x表示你要访问该页的第几个字节))

—> 结果是十进制需要转换为十六进制

I2C传输是 高位在前,低位在后【只需了解即可这里不需要操作这个】

EEPROM默认存储的是 0xFF

EEPROM 写入和读取字节的顺序是 先低位再高位

AT24C02 每次写完内容后会自动指向下一个内存空间

设备地址如下:

器件地址 "写"地址 "读"地址
0101 0000 (0x50) 1010 0000 (0xA0) 1010 0001 (0xA1)

需要注意读写之间需要至少延时 5ms 避免太快导致失败【个人推荐10ms

硬件连接

管脚 对应 模式
PB6 SCL 输出
PB7 SDA 输出
E0~E2 设备地址某3位 默认GND

CubeMX配置

只需要配置成 输出模式 ,其他不需要改

程序编写

读写方式一共有 5 种,对照时序写就行了

注意:实验试了SendACK和WaitACK是一样的,效果都可以

当前地址读取

当前地址读:内部地址计数器保存着上次访问时最后一个地址加1的值。只要芯片有电,该地址就一直保存

/*************************EEPROM.c*************************/


//当前地址读取 参数:存储接收的数据
void eeprom_read_current(uint8_t *dat)
{
	I2CStart();
	I2CSendByte(0xA1);	//发送器件地址(读操作)
	I2CWaitAck();
	*dat = I2CReceiveByte();	//读取数据
	I2CSendNotAck();
	I2CStop();
}

随机地址读取

/*************************EEPROM.c*************************/

//随机地址读取 参数:需要读取的地址
uint8_t eeprom_read_random(uint8_t addr)
{
	uint8_t temp;	//保存数据的临时变量
	
	I2CStart();
	I2CSendByte(0xA0);	//发送器件地址(写操作)
	I2CWaitAck();
	
	I2CSendByte(addr);	//写入需要读取数据所在的地址
	I2CWaitAck();
	
	I2CStart();
	I2CSendByte(0xA1);	//发送器件地址(读操作)
	I2CWaitAck();
	
	temp = I2CReceiveByte();	//读取数据
	I2CSendNotAck();
	I2CStop();
	
	return temp;	//返回数据
}

顺序读取

/*************************EEPROM.c*************************/

//顺序地址读取 //参数1:想读取的数据地址 参数2:需要保存数据的数组 参数3:要读多少个数据(一个地址一个数据)
void eeprom_read_sequential(uint8_t addr,uint8_t *dat,uint8_t len)
{
	I2CStart();
	I2CSendByte(0xA0);	//发送器件地址(写操作)
	I2CWaitAck();
	
	I2CSendByte(addr);	//写入需要读取数据所在的地址
	I2CWaitAck();
	
	I2CStart();
	I2CSendByte(0xA1);
	I2CWaitAck();
	
	while(len--)
	{
		*dat++ = I2CReceiveByte();	//读取数据到数组
		
		if(len)
		{
			I2CSendAck();
		}
		else
		{
			I2CSendNotAck();
		}
	}
	I2CStop();
}

字节写入

/*************************EEPROM.c*************************/

//字节写入 参数1:需要写的数据(字符需要用'')  参数2:需要写入的地址
void eeprom_write_byte(uint8_t dat,uint8_t addr)
{
	I2CStart();
	I2CSendByte(0xA0);	//发送器件地址(写操作)
	I2CWaitAck();
	
	I2CSendByte(addr);	//发送写入的地址
	I2CWaitAck();
	I2CSendByte(dat);	//发送数据
	I2CWaitAck();
	I2CStop();
}

页写入

  • 可以写入字符串

/*************************EEPROM.c*************************/

//页写入 参数1:需要写入的地址 参数2:需要写入的数据(数组) 参数3:数据的长度
void eeprom_write_page(uint8_t addr,uint8_t *dat,uint8_t len)
{
	I2CStart();
	I2CSendByte(0xA0);	//发送器件地址(写操作)
	I2CWaitAck();
	
	I2CSendByte(addr);	//发送写入的地址
	I2CWaitAck();
	while(len--)
	{
		I2CSendByte(*dat++);	//发送数据
		I2CWaitAck();
		addr ++;                    //E2PROM地址递增
		if((addr & 0x07) == 0)	//检查地址是否到达页边界,24C02每页8字节
		{
			break;	//到达页边界时,跳出循环,结束本次写操作
		}
					
	}
	I2CStop();
}
/*************************main.c*************************/

int main()
{
    uint8_t arr1[5] = "Fuck";
    uint8_t arr2[5];
    
    eeprom_write_page(0x08,arr1,5);
    HAL_Delay(10);
    eeprom_read_sequential(0x08,arr2,5);
    printf("%s\r\n",arr2);
}

写入/读取16位字节

  • 这样就可以直接调用函数存储 0~65535 范围的值了,读取也方便
/*************************EEPROM.c*************************/


//写入uint16类型 参数1:数据要写入的地址 参数2:数值0~65535
void eeprom_write_u16(uint8_t addr,uint16_t dat)
{
    uint8_t Hn,Ln;	//高位,低位
    Hn = (dat>>8)&0xFF;	//获取高位
    Ln = dat&0xFF;	//获取低8位
    eeprom_write_byte(Ln,addr);	//发送低位
    HAL_Delay(10);
    eeprom_write_byte(Hn,addr+1);	//发送高位
    HAL_Delay(10);
}

//读取uint16类型 参数:数据的地址
uint16_t eeprom_read_u16(uint8_t addr)
{
    uint16_t tmp;
    tmp = (uint16_t)eeprom_read_random(addr);	//先读取低位
    HAL_Delay(10);
    tmp |= (uint16_t)(eeprom_read_random(addr+1)<<8);	//再读取高位,|就是合并也可以用+
    HAL_Delay(10);

    return tmp;
}

I2C之MCP4017

MCP4017 是一款可编程的电阻,其内置了 7 位寄存器(27也就是范围是0-1272^7\text{也就是范围是0-127}),共计 127 个档位的分辨率, 注意写入的只能是整数(0~127)不能是其他数值,读出来的也是整数!!!

通信也是用 I2C

蓝桥杯采用的是 MCP4017T-104ELT,也就是说它的 最大电阻达到了100K(十万欧姆),相当于每增加一个数,电阻增加 787.402Ω

向MCP中输入的数越大,它对应的电阻也就越大,当我们传入 0x7F 时,对应的电阻就是 100K

电阻的计算:Rs的阻值为 AB之间的总电阻除以127,其中Rw的阻值 几乎为零,可以忽略不计。N为我们写入的数据;RAB为常量。

电阻的公式:电阻R=787.4×读取的数值\text{电阻的公式:电阻}R=787.4\times \text{读取的数值}

电压的公式:电压V=3.3×RR+10k\text{电压的公式:电压}V=3.3\times\frac{R}{R+10k}

硬件连接

将其 6 引脚,5 引脚 的内部电路图 简化为:

相当于一个 滑动变阻器 ,RAB 是一个大电阻,通过滑动划片 W ,可改变 RWB 的阻值

管脚 对应 模式
PB6 SCL 输出
PB7 SDA 输出
PB14 w 读取电压管脚(ADC)

从机地址:

“写”地址 "读"地址
0x5E 0x5F

CubeMX配置

只需要配置成 输出模式

配置 PB14 为ADC通道,完成相关配置【因为我在用ADC1_11通道故这里我直接配置两个通道,看实际需求配置】

程序编写

写操作

/*************************EEPROM.c*************************/


//MCP4017写操作
void mcp_write_byte(uint8_t value)
{
	I2CStart();
	I2CSendByte(0x5E);	//写操作
	I2CWaitAck();
	
	I2CSendByte(value);	//发送数据
	I2CWaitAck();
	I2CStop();
}

读操作

/*************************EEPROM.c*************************/


//MCP4017读操作
uint8_t mcp_read_byte(void)
{
	uint8_t temp;
	
	I2CStart();
	I2CSendByte(0x5F);	//读操作
	I2CWaitAck();
	
	temp = I2CReceiveByte();	//读取数据
	I2CSendNotAck();
	I2CStop();
	
	return temp;	
}
/*************************MY_LCD.c*************************/


//显示变量
void MY_LCD_DisNum(uint32_t num)
{
    uint8_t ADC5_Num[20] = "";	//ADC1通道5

    if(ADC_flag)
    {
        ADC_flag = 0;
//直接读取adc的值即可不需要那个公式计算电压
        sprintf((char *)ADC5_Num, "adc1-5:%.2f", (double)ADC1_Value[1] / 4096 * 3.3f);
        MY_LCD_Arr_Completion((uint8_t *)ADC5_Num);
        LCD_DisplayStringLine(Line8, (uint8_t *)ADC5_Num);
    }
}
/*************************main.c*************************/

uint32_t ADC1_Value[2];	//ADC1

int main()
{
    HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);    //ADC1校准
    HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&ADC1_Value,2);
    mcp_write_byte(64);	//设置阻值
}
  • 结果是有点误差的,计算过程是:

R=787.4×64=50393.6ΩR = 787.4\times64 = 50393.6Ω

V=3.3v×RR+10K=3.3×50393.650393.6+100002.753vV = 3.3v\times\frac{R}{R+10K} = 3.3 \times \frac{50393.6}{50393.6+10000} \approx 2.753v

RTC

32位的可编程,向上计数计数器,可用于较长时间段的测量

若最小单位为秒:232=4294967295=49710136\text{若最小单位为秒:}2^{32} = 4294967295\text{秒}=49710\text{天}\approx136\text{年}

可以选择以下 3 种RTC的时钟源:

时钟源
HSE时钟/128【 外部高速
LSE振荡器时钟(稳定精准,重要的是VDD掉电后可有后备供电区域给它供电)【 外部低速 典型频率为32 kHz】
LSI振荡器时钟【 内部低速 32.768 kHz(更精确)】

使用HSE分频时钟或者LSI的时候,在主电源VDD掉电的情况下,这两个时钟来源都会受到影响,因此没法保证RTC正常工作.所以RTC一般都时钟低速外部时钟LSE

3 个专门的可屏蔽中断:

中断
闹钟中断,用来产生一个软件可编程的闹钟中断
秒中断,用来产生一个可编程的周期性中断信号(最长可达1秒)
溢出中断,指示内部可编程计数器溢出并回转为0的状态

CubeMX配置

由于板子的 LSE时钟 连接了LCD的引脚所以我们不能使用它(在RCC配置那可以看到有红色)

注意,一定要选择好两次分频的系数,使经过两次分频后的时钟频率为1Hz(1秒)

32000(31+1)×(999+1)=1HZ\frac{32000}{(31+1)\times(999+1)}=1HZ

程序编写

读取时间日期

  • RTC获取必须 先获取时间再获取日期,而且时间和日期调用的时候不能单独调用, 必须两个同时调用,不然下次获取的时间和日期会出错
  • 因为我获取时间是在定时器里1s获取一次,所以刚上电会显示 00:00:00 然后再显示获取的时间日期,所以需要在mian函数里先获取一次时间日期才能保证刚上电是设定的时间日期
  • 由于蓝桥杯板子没有带掉电保护功能所以想要保存只能保存到EEPROM里
/*************************main.c*************************/

RTC_TimeTypeDef rtc_time;
RTC_DateTypeDef rtc_date;

int main()
{
  	HAL_RTC_GetTime(&hrtc,&rtc_time,RTC_FORMAT_BIN);	//获取时间,二进制格式
  	HAL_RTC_GetDate(&hrtc,&rtc_date,RTC_FORMAT_BIN);	//获取日期,二进制格式
    
    while(1)
    {
        ...
    }
}
/*************************main.h*************************/

extern RTC_TimeTypeDef rtc_time;	//获取时间的结构体
extern RTC_DateTypeDef rtc_date;	//获取日期的结构体
/*************************TIMER.c*************************/

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	static uint16_t count_1s = 0;	
	
	if(htim == &htim2)	//如果是TIM2产生中断
	{	
		count_1s++;

		if(1000 == count_1s)	//1s获取一次日期时间
		{
			count_1s = 0;
			HAL_RTC_GetTime(&hrtc,&rtc_time,RTC_FORMAT_BIN);	//获取时间,二进制格式
		    HAL_RTC_GetDate(&hrtc,&rtc_date,RTC_FORMAT_BIN);	//获取日期,二进制格式
		}
	}
}
/*************************MY_LCD.c*************************/

//显示变量
void MY_LCD_DisNum(uint32_t num)
{
	uint8_t RTC_Timer[20] = "";	//RTC时间
	uint8_t RTC_Date[20] = "";	//RTC日期

	sprintf((char *)RTC_Timer, "time:%02d:%02d:%02d",rtc_time.Hours,rtc_time.Minutes,rtc_time.Seconds);
    MY_LCD_Arr_Completion((uint8_t *)RTC_Timer);
    LCD_DisplayStringLine(Line8, (uint8_t *)RTC_Timer);
	
    sprintf((char *)RTC_Date, "date:%02d-%02d-%02d week:%d",rtc_date.Year,rtc_date.Month,rtc_date.Date,rtc_date.WeekDay);
    MY_LCD_Arr_Completion((uint8_t *)RTC_Date);
    LCD_DisplayStringLine(Line9, (uint8_t *)RTC_Date);	
    }
}

修改时间日期

  • 想修改哪个值就直接赋值哪个结构体成员,然后调用设置时间日期函数写入即可【修改日期则星期几页要一起修改不然对不上】
rtc_time.Minutes = 59;
rtc_date.Year = 24;
HAL_RTC_SetTime(&hrtc, &rtc_time, RTC_FORMAT_BIN);
HAL_RTC_SetDate(&hrtc, &rtc_date, RTC_FORMAT_BIN);

暂停时间日期

  • 通过这两个函数来进行时间日期的暂停和开始【暂停和开始都不会重新清零的】
//暂停
__HAL_RCC_RTC_DISABLE();
//开始
__HAL_RCC_RTC_ENABLE();

RTC闹钟功能

CubeMX配置:

开启闹钟中断 【RTC外设没有独立的中断,但是ST巧妙的将RTC外设都连接到了外部中断EXTI】,通过触发EXTI来产生RTC外设中断

注意:回调函数可在 stm32g4xx_it.c 里跳转,注意函数名有ABC三个闹钟的不要选错

/*************************main.c*************************/

#define alarm_hour 0
#define alarm_min 0
#define alarm_sec 10

void Alarm_configuration(uint8_t hour,uint8_t min,uint8_t sec);
void Alarm_compute(uint8_t hour,uint8_t min,uint8_t sec);

int main()
{
    while(1)
    {
	  	if(alarm_clock_flag)
	  	{
			  alarm_clock_flag = 0;
			  HAL_UART_Transmit_DMA(&huart1,(uint8_t*)"闹钟响了\r\n",12);
	  	}        
    }
}

//闹钟配置函数 参数:时 分 秒
void Alarm_configuration(uint8_t hour, uint8_t min, uint8_t sec)
{
    //直接复制 rtc.c 然后把 RTC_FORMAT_BCD 改成 RTC_FORMAT_BIN
    RTC_AlarmTypeDef sAlarm = {0};

    sAlarm.AlarmTime.Hours = hour;
    sAlarm.AlarmTime.Minutes = min;
    sAlarm.AlarmTime.Seconds = sec;
    sAlarm.AlarmTime.SubSeconds = 0x0;
    sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY;
    sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_ALL;
    sAlarm.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_DATE;
    sAlarm.AlarmDateWeekDay = 0x1;
    sAlarm.Alarm = RTC_ALARM_A;
    if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN) != HAL_OK)
    {
        Error_Handler();
    }
}
void Alarm_compute(uint8_t hour, uint8_t min, uint8_t sec)
{
    uint8_t h = 0, m = 0, s = 0;
    RTC_TimeTypeDef gtime = {0};
    RTC_DateTypeDef gdate = {0};

    HAL_RTC_GetTime(&hrtc, &gtime, RTC_FORMAT_BIN);
    HAL_RTC_GetDate(&hrtc, &gdate, RTC_FORMAT_BIN);

    if((s = (sec + gtime.Seconds)) > 59)	//如果设定值+获取值>59
    {
        s = s % 60;	//取余
        ++gtime.Minutes;	//分+1
    }
    if((m = (min + gtime.Minutes)) > 59)	//如果设定值+获取值>59
    {
        m = m % 60;	//取余
        ++gtime.Hours;	//时+1
    }
    if((h = (hour + gtime.Hours)) > 23)	//如果设定值+获取值>23
    {
        h = h % 23;	//取余
    }

    Alarm_configuration(h, m, s);
}


//闹钟中断回调函数
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
    alarm_clock_flag = 1;	//闹钟标志位
    Alarm_compute(alarm_hour, alarm_min, alarm_sec);
}

实现隔10s触发一次闹钟

低功耗(了解)

stm32有三种低功耗模式,功耗依次降低:

模式 描述
睡眠模式 只有内核时钟关闭,外设仍在运行;可以通过任意一个中断或唤醒事件唤醒; 唤醒后回到睡眠的位置向后执行
停止模式 关闭内核时钟、外设时钟,保留内核1.8V供电,寄存器和RAM中的数据可以保持,IO口状态也可保持;可以通过任意一个外部中断唤醒; 唤醒后可回到停止的代码处向后执行,但要重新初始化时钟和外设
待机模式 关闭所有时钟,关闭内核1.8V供电,寄存器和RAM数据不能保持(除了电源控制/状态寄存器(PWR_CSR)、备份寄存器,其他数据都丢失);可通过唤醒引脚(PA0)上升沿、RTC闹钟中断,或者复位唤醒; 唤醒后相当于复位,从复位地址开始执行

//进入待机模式函数
void enter_standby_mode(void)
{
	HAL_PWR_DisableWakeUpPin(PWR_WAKEUP_PIN1);	//失能PA0
	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);//PA0端口变成低电平,准备好唤醒键初始电平
	__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);	//清除唤醒标志位
	HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);	////使能WKUP引脚的唤醒功能
	HAL_PWR_EnterSTANDBYMode();	//进入待机模式
}

PWM

预备知识

比赛题目会给定哪个引脚的,如果没有则自己挑【注意不要和别的外设功能冲突就行】

还有题目会给误差范围(一般是±5%\pm5\%),你需要保证你的频率或者占空比在它误差范围内就行

一般考点
单个定时器单个通道输出特定频率,特定/不同占空比的PWM波【PWM模式】
单个定时器多个通道输出特定频率,特定/不同占空比的PWM波【PWM模式】
单个定时器单个通道输出不同频率,不同占空比的PWM波【PWM模式】
单个定时器多个通道输出不同频率,不同占空比的PWM波【PWM输出比较模式】
PWM3种模式
PWM模式: 同一定时器中,不同的通道下,输出的频率固定,占空比可变
PWM输出比较模式: 同一定时器下,不同通道能够产生频率不同,占空比不同,甚至相位也不同的方波
PWM捕获模式: PWM比较模式是输出不同频率或者占空比,这个是捕获就是反过来已知波形图而去计算对应占空比和频率
寄存器 描述
CNT 计数器寄存器(从0开始计数到ARR则重新计数)
CCRx 即Pulse (决定了占空比)
极性 高电平/低电平 (决定了PWM有效电平)
ARR 重装载值 (决定了PWM频率)
名词 描述
PWM 脉宽调制信号
占空比 占空比是指有效电平在一个周期之内所占的时间比率。如果方波的占空比为60%,占空比为0.6,即信号在60%时间内是导通的,但在40%的时间内处于断态
方波 跟占空比差不多,一般用百分数表示,占空比0.5就是方波50%
pwm频率 是指每秒钟信号从高电平到低电平再回到高电平的次数
定时器 用途
TIM1,TIM8 产生输出波形(输出比较,PWM,带死区插入的互补PWM)
TIM6,TIM7 DAC
其余 输出比较

Hz(赫兹),KHz(千赫兹),MHz(兆赫兹),GHz(G赫兹)

单位换算:

1Hz=1s1Hz = 1s

1KHz=1ms1KHz = 1ms

1MHz=1us1MHz = 1us

1GHz=1ns1GHz = 1ns

左边越大,对应时间越小(左边除以2则右边乘以2)

除了基本定时器(TIM6、7)外,其他所有定时器均能进行PWM信号输出,具体哪个管脚通道可以直接去点击CubeMX查看或者查看手册(板子看LQFP64那列)

频率公式(单位:Hz)=定时器时钟(单位:Hz)/(psc预分频值+1)/(arr计数值+1)\text{频率公式(单位:Hz)}=\text{定时器时钟(单位:Hz)}/(\text{psc预分频值}+1)/(\text{arr计数值}+1)

周期(单位:s)=1÷频率(单位:Hz)\text{周期(单位:s)}=1 \div \text{频率(单位:Hz)}

占空比(单位:无)=Pulse对比值arr计数值+1×100%\text{占空比(单位:无)}=\frac{\text{Pulse对比值}}{\text{arr计数值+1}}\times100\%

TIM-PWM

PWM模式下,ARR决定输出频率,CCRx决定输出占空比

计数方式 PWM1(模式1) PWM2(模式2)
向上/向下 CNT < CCRx —> 有效电平 CNT > CCRx —> 有效电平

PWM模式1和PWM模式2,为 互补 的波形, PWM模式1+极性低 == PWM模式2+极性高

假设当前设置为:PWM模式2,输出高极性,如图则CNT<CCRx的部分为无效电平(低电平),大于CCRx的部分为有效电平(高电平)

单个定时器多个通道输出特定频率,特定/不同占空比的PWM波

实验管脚 对应通道
PA6 TIM3_CH1
PA7 TIM3_CH2

要求:频率设定为 2500Hz,占空比范围为10%~90%,通过按键每次增加10%,加到最大范围后再按一次回到10%;初始时CH1占空比为0.1,CH2占空比为0.5

周期=12500=0.0004s=0.4ms\text{周期}=\frac{1}{2500} = 0.0004s=0.4ms

频率计算:2500=80000000/x>x=32000即:psc=321arr=10001\text{频率计算:}2500=80000000/x \quad \textcolor{red}{->} x=32000 \quad \text{即:}psc=32-1 \quad arr=1000-1

占空比计算:通道一:0.1=x1000x=100通道二:0.5=x1000x=500\text{占空比计算:} \text{通道一:}0.1 = \frac{x}{1000} \quad x=100\quad\text{通道二:}0.5 = \frac{x}{1000} \quad x=500

CubeMX配置:

  • 通过初始化函数里找,可以发现最后设置的占空比会被赋给 CCRx

而在设置占空比函数__HAL_TIM_SET_COMPARE()最后也是赋给了CCR这个寄存器(不同通道对应不同的CCRx)

/********************main.c***********************/

int main()
{
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);	//开启定时器3通道1
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);	//开启定时器3通道2

    while(1)
    {
        if(PWM1_Flag)	//PWM标志位
        {
            PWM1_Flag = 0;
            pA6_SetDuty(100);
        }
        if(PWM2_Flag)	//PWM标志位
        {
            PWM2_Flag = 0;
            pA7_SetDuty(100);
        }
    }

}
/*********************PWM.c*************************/

uint16_t pa6_duty = 100;	//PA6通道1初始占空比10%
uint16_t pa7_duty = 500;	//PA7通道1初始占空比50%

//PA6设置占空比函数 参数:占空比值:100~900
void pA6_SetDuty(uint16_t d)
{
    pa6_duty += d;

    if(pa6_duty > 900)
    {
        pa6_duty = 100;
    }
    __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, pa6_duty);	//设置通道2占空比
}

//PA7设置占空比函数 参数:占空比值:100~900
void pA7_SetDuty(uint16_t d)
{
	pa7_duty+=d;
	
	if(pa7_duty > 900)
	{
		pa7_duty = 100;
	}
	__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_2,pa7_duty);	//设置通道2占空比
}
  • htimx.Instance->CCRx 就是获取通道x占空比寄存器的值
  • htimx.Instance->ARR + 1就是获取定时器x的重装载值(记得+1)
/************************MY_LCD.c**************************/

// 界面2显示pwm相关
void pwm_display(void)
{
    uint8_t pwm_A6_1[20] = "";
    uint8_t pwm_A7_1[20] = "";
    sprintf((char *)pwm_A6_1, "A6:Fre-%d%% CCR1-%d", pa6_duty / 10, htim3.Instance->CCR1);
    MY_LCD_Arr_Completion(pwm_A6_1);
    LCD_DisplayStringLine(Line0, (uint8_t *)pwm_A6_1);
    sprintf((char *)pwm_A7_1, "A7:Fre-%d%% CCR2-%d", pa7_duty / 10, htim3.Instance->CCR2);
    MY_LCD_Arr_Completion(pwm_A7_1);
    LCD_DisplayStringLine(Line1, (uint8_t *)pwm_A7_1);
}

单个定时器单个通道输出不同频率,不同占空比的PWM波

要求:跟上面一样,在其基础上增加按键按下频率加10%,加到大于等于 初始频率的0.9倍 则回到初值

/*******************PWM.c***************************/


//定时器3设置频率函数 参数:每次增加多少 单位 %
void p_SetFre(uint16_t f)
{
    uint16_t p_arr;
    uint32_t p_fre = 80000000 / (31 + 1) / (htim3.Instance->ARR + 1);    //计算频率
    float p_temp1 = (float)f / 100; //百分数转小数形式【不加类型转换则变成0】
    float temp2 = p_fre * p_temp1; //算出需要加多少
    p_fre += temp2;//在原有基础上相加
    p_arr = (80000000 / (31 + 1)) / p_fre; //算出需要的ARR
    if(p_fre > (2500 + (2500 * 0.9)))	//超过初值的0.9倍则回到初值频率,即ARR回到初值即可
    {
        p_arr = 1000;
    }
    __HAL_TIM_SetAutoreload(&htim3, p_arr - 1); //设置ARR
}
/**********************main.c************************/

int main()
{
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);//开启定时器3通道1

    while(1)
    {
        if(PWM1_Flag)	//PWM 【按键3】
        {
            PWM1_Flag = 0;
            p_SetFre(10);//每次加10%
        }
    }
}

LCD显示与上面一样

这个是手持示波器,测量范围在0~80KHz,低于20Hz的测不了

还有PWM驱动LED实现呼吸灯,我的工程没效果,新建的另一个工程正常显示【原因暂时不清楚】

TIM-OC输出

输出比较模式下,pulse决定输出频率,duty决定每个通道的占空比初始相位

输出比较模式下的8种模式 描述
冻结 Frozen (used for Timing base) 理解为CNT和CCR无效,REF保持为原状态,假设你正在输出PWM波,暂停一段时间输出,则可以设置这个模式,一旦切换为冻结模式后,输出就可以暂停了,并且高低电平也维持为暂停时刻的状态保持不变
匹配输出高电平 Active Level on match /
匹配输出低电平 lnactive Level on match /
匹配翻转电平 Toggle on match 可以方便地输出一个频率可调,占空比始终为50%的 PWM波形
强制输出高电平 Forced Active 如果想暂停波形输出,并且在暂停时期保持高电平
强制输出低电平 Forced lnactive 如果想暂停波形输出,并且在暂停时期保持低电平
可重触发单脉冲模式 (OPM1) Retriggerable OPM1 启用选项后,可以重新触发定时器以在前一个脉冲完成之前生成一个新脉冲,从而使其能够生成一系列具有相同持续时间的脉冲【在定时器的第1个通道上可用】
可重触发单脉冲模式 (OPM2) Retriggerable OPM2 同上【在定时器的第2个通道上可用】
输出比较原理
输出比较模式在CNT与CCRx不断做比较的过程中,CNT < CCRx时输出有效电平,若CCRx(捕获/比较寄存器)== CNT(计数值),产生的则是翻转输出电平,并且会产生中断,通过对中断回调函数的编写,就能够实现多路不同频率信号的输出

单个定时器多个通道输出不同频率,不同占空比的PWM波

实验管脚 对应通道
PA11 TIM4_CH1
PA12 TIM4_CH2

要求:PA11初始频率为9KHz,初始占空比为70%;PA12初始频率为600Hz,初始占空比为50%;按键1按下PA11频率增加10%,PA12频率增加20%,增加到80%则回到初值;显示:频率(单位:Hz) 占空比(保留2位小数 单位:%)

CubeMX配置:

/***********************PWM.c**************************/

uint32_t ch1_pulse_val, ch2_pulse_val;	//通道1的CRRx值   通道2的CRRx值
uint32_t ch1_duty_val, ch2_duty_val;	//通道1有效电平占空比所需值   通道2有效电平占空比所需值

//计算有效电平所需值
uint32_t pwm_oc_calculate_duty(uint8_t duty,uint32_t pulse_val)
{
    return (uint32_t)((float)pulse_val*(float)duty/100.0f);
}

//设置通道1频率,占空比 参数1:频率Hz  参数2:占空比%
void pwm_oc_setCH1(uint32_t freq, uint8_t duty)
{
    ch1_pulse_val = 1000000.0f / (float)freq;	//算出频率所需的CRRx值
    ch1_duty_val = pwm_oc_calculate_duty(duty,ch1_pulse_val);	//算出有效电平所需值
    htim4.Instance->CCR1 = ch1_pulse_val;
}

//设置通道2频率,占空比 参数1:频率Hz  参数2:占空比%
void pwm_oc_setCH2(uint32_t freq, uint8_t duty)
{
    ch2_pulse_val = 1000000 / freq;	//算出总频率所需的CRRx值
    ch2_duty_val = pwm_oc_calculate_duty(duty,ch2_pulse_val);	//算出有效电平所需值
    htim4.Instance->CCR2 = ch2_pulse_val;
}


//PWM比较模式回调函数
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
    static uint8_t pwm1_flag = 1;	//定义翻转标志位
    static uint8_t pwm2_flag = 1;	//定义翻转标志位

    if(htim == &htim4)	//如果是定时器4
    {
        if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)	//如果是通道1
        {
            if(pwm1_flag)
            {
                htim4.Instance->CCR1 += ch1_duty_val;	//写入有效电平所需的CRRx值
            }
            else
            {
                htim4.Instance->CCR1 += ch1_pulse_val - ch1_duty_val;	//写入无效电平所需的CRRx值(总频率值-高电平值=低电平值)
            }
            pwm1_flag = !pwm1_flag;	//取反
        }

        if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)	//如果是通道2
        {
            if(pwm2_flag)
            {
                htim4.Instance->CCR2 += ch2_duty_val;	//写入有效电平所需的CRRx值
            }
            else
            {
                htim4.Instance->CCR2 += ch2_pulse_val - ch2_duty_val;	//写入无效电平所需的CRRx值
            }
            pwm2_flag = !pwm2_flag;	//取反
        }
    }
}
/************************KEY.c****************************/


static uint8_t pwm1_count,pwm2_count;

case 1:
{
    pwm1_count++;
    pwm2_count++;
    if(pwm1_count > 8)	//8次X10%=80%
    {
        pwm1_count = 0;
    }
    if(pwm2_count > 4)	//4次X20%=80%
    {
        pwm2_count = 0;
    }
    pwm_oc_setCH1(9000 + (9000 * (0.1 * pwm1_count)), 70);
    pwm_oc_setCH2(600 + (600 * (0.2 * pwm2_count)), 50);
    break;
}
/*******************************MY_LCD.c***************/


//界面2显示pwm相关
void pwm_display(void)
{
    uint8_t pwm_oc_scan[20] = "";

    //TIM4比较模式
    sprintf((char *)pwm_oc_scan, "1F:%dHz D:%.2f%%", 1000000 / ch1_pulse_val, (double)ch1_duty_val / ch1_pulse_val * 100);
    MY_LCD_Arr_Completion(pwm_oc_scan);
    LCD_DisplayStringLine(Line3, (uint8_t *)pwm_oc_scan);
    memset(pwm_oc_scan, 0, sizeof(pwm_oc_scan));

    sprintf((char *)pwm_oc_scan, "2F:%dHz D:%.2f%%", 1000000 / ch2_pulse_val, (double)ch2_duty_val / ch2_pulse_val * 100);
    MY_LCD_Arr_Completion(pwm_oc_scan);
    LCD_DisplayStringLine(Line4, (uint8_t *)pwm_oc_scan);
    memset(pwm_oc_scan, 0, sizeof(pwm_oc_scan));
}
/**********************main.c*******************************/

int main()
{
    pwm_oc_setCH1(9000, 70);	//设置频率,占空比
    pwm_oc_setCH2(600, 50);	//设置频率,占空比
    HAL_TIM_OC_Start_IT(&htim4, TIM_CHANNEL_1);	//定时器4通道1
    HAL_TIM_OC_Start_IT(&htim4, TIM_CHANNEL_2);	//定时器4通道2
    
    while(1)
    {
        ...
    }
}

TIM-IC捕获

了解内容

输入捕获可以对输入的信号的 上升沿下降沿 或者 双边沿 进行捕获,常用的有 测量输入信号的脉宽测量PWM输入信号的频率和占空比 这两种

定时器的输入捕获有两个功能
直接捕获:只能捕获本身通道的脉冲信号
间接模式:可以捕获此定时器每个通道的脉信号
输入捕获的两种方式 描述
普通输入捕获( 测量频率很高的不太精确 ) 可使用定时器的四个通道,一路捕获占用一个捕获寄存器
PWM 输入模式( 测量频率很高的精确 ) 只能使用两个通道,CH1和CH2

因为只有 TI1FP1TI2FP2 连到了 从模式控制器,所以PWM输入模式只能使用 TIMx_CH1 /TIMx_CH2 信号【来自STM32_CN手册216页】

同一个定时器只能要么只做PWM输出,要么只做输入捕获,虽然可以不同通道但是会有很多问题,所以不推荐

普通输入模式( 实测比PWM输入模式准 )

原理:首先CubeMX设置配置一个通道,然后上升沿捕获,在中断回调函数里首先保存第一次上升沿的值(ONE),然后设置成下降沿捕获,第二次进入回调函数时保存第一次下降沿的值(TWO),然后设置成上升沿捕获,第三次进入回调函数时保存第二次上升沿的值(THREE),然后设置成上升沿捕获,第四次进入回调函数时保存第二次下降沿的值(FOUR),然后停止通道,在LCD那进行计算显示然后再开启通道继续下一次的捕获,以此循环。

FOURTHREE=高电平值FOUR - THREE = \text{高电平值}

频率(单位:Hz)=预分频后的频率一个周期值=1000000FOURTWO\text{频率(单位:Hz)} = \frac{\text{预分频后的频率}}{\text{一个周期值}} = \frac{1000000}{FOUR-TWO}

占空比(单位:百分号)=高电平值一个周期值=FOURTHREEFOURTWO×100\text{占空比(单位:百分号)=}\frac{\text{高电平值}}{\text{一个周期值}} = \frac{FOUR-THREE}{FOUR-TWO} \times100

CubeMX配置:

记得开启中断

/******************main.c****************/

typedef struct
{
	uint32_t One_value;	//4次获取
	uint32_t Two_value;
	uint32_t Three_value;
	uint32_t Four_value;
	uint32_t Fre;	//算出的结果
	float Duty;
	uint8_t RUN_FLAG;
}PARAMETER_TypeDef;

PARAMETER_TypeDef CH3_Data = {0};	//两个通道
PARAMETER_TypeDef CH2_Data = {0};
bool IN_MODE = 1;	//切换通道

int main()
{
	uint8_t arr[40] = "\0";
  HAL_TIM_Base_Start_IT(&htim6); 
   HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_3);
  HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_2);
    while(1)
    {
	  if(4 == CH3_Data.RUN_FLAG)
	  {
		  CH3_Data.Fre = 1000000.0f / (float)(CH3_Data.Four_value - CH3_Data.Two_value);
		  CH3_Data.Duty = (float)(CH3_Data.Four_value - CH3_Data.Three_value)/(float)(CH3_Data.Four_value - CH3_Data.Two_value)*100;
		  snprintf((char*)arr,sizeof(arr),"CH3:fre-%d duty-%.2f%%\r\n",CH3_Data.Fre,CH3_Data.Duty);
		  HAL_UART_Transmit(&huart1,(uint8_t*)arr,strlen((char*)arr),500);
		  CH3_Data.RUN_FLAG = 0;
		  HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_3);
	  }
	  if(4 == CH2_Data.RUN_FLAG)
	  {
		  CH2_Data.Fre = 1000000.0f / (float)(CH2_Data.Four_value - CH2_Data.Two_value);
		  CH2_Data.Duty = (float)(CH2_Data.Four_value - CH2_Data.Three_value)/(float)(CH2_Data.Four_value - CH2_Data.Two_value)*100;
		  snprintf((char*)arr,sizeof(arr),"CH2:fre-%d duty-%.2f%%\r\n",CH2_Data.Fre,CH2_Data.Duty);
		  HAL_UART_Transmit(&huart1,(uint8_t*)arr,strlen((char*)arr),500);
		  CH2_Data.RUN_FLAG = 0;
		  HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_2);
	  }	
	  HAL_Delay(300);
}
/*******************PWM.c********************/


//捕获中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if(htim == &htim2)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_3)
		{
			if(IN_MODE)
			{
				switch(CH3_Data.RUN_FLAG)
				{
					case 0:
					{
						CH3_Data.One_value = TIM2->CCR3;	//获取第一次上升沿值
						__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_3,TIM_INPUTCHANNELPOLARITY_FALLING);	//下降沿
						CH3_Data.RUN_FLAG = 1;
						break;
					}
					case 1:
					{
						CH3_Data.Two_value = TIM2->CCR3;	//获取第一次下降沿值
						__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_3,TIM_INPUTCHANNELPOLARITY_RISING);	//上升沿				
						CH3_Data.RUN_FLAG = 2;
						break;
					}
					case 2:
					{
						CH3_Data.Three_value = TIM2->CCR3;	//获取第二次上升沿值
						__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_3,TIM_INPUTCHANNELPOLARITY_FALLING);	//下降沿						
						CH3_Data.RUN_FLAG = 3;
						break;
					}
					case 3:
					{
						CH3_Data.Four_value = TIM2->CCR3;	//获取第二次下降沿值
						__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_3,TIM_INPUTCHANNELPOLARITY_RISING);	//上升沿	
						HAL_TIM_IC_Stop_IT(&htim2,TIM_CHANNEL_3);
						__HAL_TIM_SetCounter(&htim2, 0);	//计数值清0
						CH3_Data.RUN_FLAG = 4;
						break;
					}
					default:break;					
				}
			}
		}
		
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
		{
			if(!IN_MODE)
			{
				switch(CH2_Data.RUN_FLAG)
				{
					case 0:
					{
						CH2_Data.One_value = TIM2->CCR2;	//获取第一次上升沿值
						__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_2,TIM_INPUTCHANNELPOLARITY_FALLING);	//下降沿
						CH2_Data.RUN_FLAG = 1;
						break;
					}
					case 1:
					{
						CH2_Data.Two_value = TIM2->CCR2;	//获取第一次下降沿值
						__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_2,TIM_INPUTCHANNELPOLARITY_RISING);	//上升沿				
						CH2_Data.RUN_FLAG = 2;
						break;
					}
					case 2:
					{
						CH2_Data.Three_value = TIM2->CCR2;	//获取第二次上升沿值
						__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_2,TIM_INPUTCHANNELPOLARITY_FALLING);	//下降沿						
						CH2_Data.RUN_FLAG = 3;
						break;
					}
					case 3:
					{
						CH2_Data.Four_value = TIM2->CCR2;	//获取第二次下降沿值
						__HAL_TIM_SET_CAPTUREPOLARITY(&htim2,TIM_CHANNEL_2,TIM_INPUTCHANNELPOLARITY_RISING);	//上升沿	
						HAL_TIM_IC_Stop_IT(&htim2,TIM_CHANNEL_2);
						__HAL_TIM_SetCounter(&htim2, 0);	//计数值清0
						CH2_Data.RUN_FLAG = 4;
						break;
					}
					default:break;					
				}
			}
		}		
	}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	static uint16_t mode_count = 0;
	
	if(htim == &htim6)
	{
		mode_count++;
		
		if(150 == mode_count)
		{
			mode_count = 0;
			IN_MODE = !IN_MODE;
		}
	}
}

PWM 输入模式

当第一次上升沿时,IC1和IC2同时捕获中断,计数器CNT清零;到了下降沿的时候,IC2捕获中断,此时计数器CNT的值被锁存到捕获寄存器CCR2中,到了下一个上升沿的时候,IC1捕获中断,计数器CNT的值被锁存到捕获寄存器CCR1中

硬件连接

板子上有两个信路达的XL555信号发生器【手册在阿里云盘】,生成PWM波,利用定时器输入捕获功能可以检测出,信号发生器的所产生脉冲信号的频率和占空比

这个是示波器测的:

信号发生器 往左扭到最大时 往右扭到最大时
R39 频率约等于23KHz,占空比约等于74% 频率约等于735Hz,占空比约等于50%
R40 频率约等于22KHz,占空比约等于73.9% 频率约等于735Hz,占空比约等于50%
管脚 对应通道
PA15 TIM2_CH1,TIM8_CH1
PB4 TIM3_CH1

通常,测频率支路设置成 直接捕获模式 ,而测脉宽的支路设置成 间接模式

CubeMX配置:

/********************main.c*************************/


int main()
{
    MX_TIM2_Init();	//把初始化移过来这样一起开启提高准确率
    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2);
    MX_TIM3_Init();  
    HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
    HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);

    while(1)
    {
        ...
    }
}
/*******************TIMER.c*******************/

uint8_t pwm_ic_flag = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    static uint16_t count_200ms = 0;

    if(htim == &htim7)	//如果是TIM2产生中断
    {
        count_200ms++;

        if(200 == count_200ms)	//200ms刷新一次
        {
            count_200ms = 0;
            pwm_ic_flag = 1;
        }
    }
}
/*****************MY_LCD.c********************/

//界面2显示pwm相关
void pwm_display(void)
{
    uint8_t pwm_ic_scan[20] = "";

    //捕获模式
    if(pwm_ic_flag)
    {
        pwm_ic_flag = 0;
        sprintf((char *)pwm_ic_scan, "f1:%dHz d1:%.2f", 1000000 / ch1_Period, ch1_Hightime / (float)ch1_Period * 100);
        MY_LCD_Arr_Completion(pwm_ic_scan);
        LCD_DisplayStringLine(Line5, (uint8_t *)pwm_ic_scan);
        memset(pwm_ic_scan, 0, sizeof(pwm_ic_scan));

        sprintf((char *)pwm_ic_scan, "f2:%dHz d2:%.2f", 1000000 / ch2_Period, ch2_Hightime / (float)ch2_Period * 100);
        MY_LCD_Arr_Completion(pwm_ic_scan);
        LCD_DisplayStringLine(Line6, (uint8_t *)pwm_ic_scan);
        memset(pwm_ic_scan, 0, sizeof(pwm_ic_scan));
    }
}
/******************PWM.c*********************/

uint32_t ch1_Period = 0, ch2_Period = 0;	//总周期计数值
uint32_t ch1_Hightime = 0, ch2_Hightime = 0;	//高电平的计数值

//捕获中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if(&htim2 == htim)
    {
        if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)	//通道1
        {
            ch1_Period = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);	//读取直接通道
            __HAL_TIM_SetCounter(&htim2, 0);	//计数值清0
        }
        else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
        {
            ch1_Hightime = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_2);	//读取间接通道
        }
    }

    if(&htim3 == htim)
    {
        if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)	//通道1
        {
            ch2_Period = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1);	//读取直接通道
            __HAL_TIM_SetCounter(&htim3, 0);	//计数值清0
        }
        else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
        {
            ch2_Hightime = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_2);	//读取间接通道
        }
    }
}

感觉误差挺大的,示波器测量的频率最大值跟程序算出来的不太一样,而且LCD显示数字一直在变,要是知道范围的话就可以把算出来的频率跟最大值比较,大于就舍弃小于就保留显示

PWM其他

输出PWM

MX_TIM15_Init();
HAL_TIM_PWM_Start(&htim15, TIM_CHANNEL_1);	//不能忘记重新开始PWM
TIM15->CCR1 = Parameter_Data.PA2_value * 10;

输出低电平

vGpio_init(GPIO_PIN_1);

void vGpio_init(uint16_t pin)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    GPIO_InitStruct.Pin = pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    HAL_GPIO_WritePin(GPIOA, pin, GPIO_PIN_RESET);	//低电平
}

附1:关于打开别人的例程报错问题

打开别人工程显示 cannot read project file 解决方法是看看工程路径有没有空格等东西,一般最外层文件名可能会有空格需要把空格删了再重新下载例程到这个文件下(一旦改了最外层会导致该路径下的文件全部报废打不开所以需要提前把文件备份)

还有一个问题就是编译成功但是下载到单片机时显示 Error: Flash Download failed - "Cortex-M4" 原因是虽然安装了G4芯片包,而且在 DebugSettingsFlash Download 有显示128k的芯片包,但是当点击下面的 Add 进去发现并没有G4的Flash,所以需要把之前安装的 Flash路径下( C:...\AppData\Local\Arm\Packs\Keil\STM32G4xx_DFP\1.2.0\CMSIS\Flash\)的 .FLM 文件和3个文件夹复制到 C:\Keil_v5\ARM\Flash\ 下即可

注意

如果不知道之前芯片包安装路径可重新点安装会有显示

附2: Keil仿真问题

仿真需要设置:Debug — 不用勾Use Simulator — 勾Run to main() — 下面的Dialog DLL填写 DARMSTM.DLLParameter填写 -pSTM32G431RBTx(具体看你芯片型号);旁边那列也一样

附3:关于Keil编译后数据占容量解析

名称 含义 程序组件
Code 代码域,它指的是编译器生成的机器指令,这些内容被存储到 ROM 机器代码指令
RO-data 只读数据域,它指程序中用到的只读数据,这些数据被存储在 ROM区,因而程序不能修改其内容 (如const关键字定义的变量) 常量
RW-data 可读写数据域,它指初始化为“非 0 值”的可读写数据,程序刚运行时,这些数据具有非0的初始值,且运行的时候它们会常驻在 RAM 区,因而应用程序可以修改其内容 (如全局变量,且定义时赋予 非0值给该变量进行初始化) 初值非0的全局变量
ZI-data 指初始化为 0值可读写数据域,它与 RW-data 的区别是程序刚运行时这些数据初始值全都为 0,而后续运行过程与 RW-data 的性质一样,它们也常驻在 RAM 区,因而应用程序可以更改其内容 (如全局变量定义时赋予 0值 给该变量进行初始化,若定义该变量时没有赋予初始值,编译器会把它当 ZI-data 来对待,初始化为 0) 初值为0的全局变量
名称 内容 属于
栈空间 进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间 ZI-data
这些空间都会被初始值化为 0 值
堆空间 使用 malloc 动态分配的变量 ZI-data
这些空间都会被初始值化为 0 值

注意:编译器给出的 ZI-data 占用的空间值中包含了堆栈的大小(经实际测试,若程序中完全没有使用 malloc动态申请堆空间,编译器会优化,不把堆空间计算在内)

名称 组成
当程序存储到STM32内部的Flash(即ROM区) Code + RO-data + RW-data
当程序在执行的时需要占用STM32内部的SRAM(即RAM区) RW-data + ZI-data

注意:所以编译时需要注意内容总大小不能超过 STM32的Flash

附4:HAL函数

函数原型 功能
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init); GPIO初始化
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin); 在函数初始化之后的引脚恢复成默认的状态,即各个寄存器复位时的值
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); 读取引脚的电平状态、函数返回值为0或1
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState); 引脚写0或1
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); 翻转引脚的电平状态
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); 锁住引脚电平,比如说一个管脚的当前状态是1,当这个管脚电平变化时保持锁定时的值
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin); 外部中断服务函数,清除中断标志位
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin); 外部中断回调函数,可以理解为中断函数具体要响应的动作
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim) 以中断模式启动 TIM
__weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 所有定时器中断的回调函数(相当于固件库的中断服务函数)
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout) 串口发送函数(不会触发中断,同时具备超时参数)
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size) 串口DMA发送
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) 串口接收函数(不会触发中断,同时具备超时参数)
HAL_UART_RxHalfCpltCallback() 一半数据接收完成时调用(不常用)
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 数据接收中断触发的回调函数
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) 数据发送中断触发的回调函数
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc) 启动ADC转换函数
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef *hadc) 获取ADC采集值
HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc, uint32_t SingleDiff) ADC校准
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef *hadc, uint32_t *pData, uint32_t Length) 开启ADC的DMA功能
uint32_t HAL_ADC_GetState(ADC_HandleTypeDef *hadc) 获取ADC状态
HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc, uint32_t Timeout) 等待转换完成,第二个参数表示超时时间,单位ms
#define HAL_IS_BIT_SET(REG, BIT) (((REG) & (BIT)) == (BIT)) 判断转换完成标志位是否设置
HAL_StatusTypeDef HAL_DAC_Start(DAC_HandleTypeDef *hdac, uint32_t Channel) 开启DAC通道
HAL_StatusTypeDef HAL_DAC_SetValue(DAC_HandleTypeDef *hdac, uint32_t Channel, uint32_t Alignment, uint32_t Data) 设置DAC输出值
uint32_t HAL_DAC_GetValue(DAC_HandleTypeDef *hdac, uint32_t Channel) 获取DAC输出值
HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format) 获取RTC时间
HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format) 获取RTC日期
HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format) 设置RTC时间
HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format) 设置RTC日期
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) 闹钟中断回调函数
HAL_StatusTypeDef HAL_RTC_SetAlarm_IT(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, uint32_t Format) 启动闹钟+中断
#define __HAL_TIM_SetCompare __HAL_TIM_SET_COMPARE 设置PWM占空比
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel) 开启定时器PWM输出
#define __HAL_TIM_SetAutoreload __HAL_TIM_SET_AUTORELOAD 设置PWM ARR值(频率)
HAL_StatusTypeDef HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel) 暂停定时器PWM输出
#define __HAL_TIM_GetCounter __HAL_TIM_GET_COUNTER 获取CNT计数值
#define __HAL_TIM_GetCompare __HAL_TIM_GET_COMPARE 获取CCRx寄存器值
__weak void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim) 输出比较中断回调函数
__weak void HAL_TIM_PeriodElapsedHalfCpltCallback(TIM_HandleTypeDef *htim) 非阻塞模式下周期已过半完成回调
uint32_t HAL_TIM_ReadCapturedValue(TIM_HandleTypeDef *htim, uint32_t Channel) 从Capture Compare单元读取捕获值
HAL_StatusTypeDef HAL_TIM_OC_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel) 启动输出比较+中断模式
HAL_StatusTypeDef HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel) 启动输入捕获+中断模式
#define __HAL_TIM_SetCounter __HAL_TIM_SET_COUNTER 设置TIM计数器寄存器值(CNT)
uint32_t HAL_TIM_ReadCapturedValue(TIM_HandleTypeDef *htim, uint32_t Channel) 从Capture Compare单元读取捕获值

附5:其他问题

在你自己创建的文件里使用HAL库的结构体需要包含生成的外设的 .h 文件,否则用不了

在ADC,DAC等地方出现数据 左对齐 右对齐 的地方,它们区别是:

12位二进制最大值为 0x0FFF 左对齐操作后的结果是 0xFFF0,右对齐后还是0x0FFF。反过来看 ,若寄存器里左对齐的数据值X (相当于实际数据*16,所以左对齐转换的值要/16才是实际的值),则 X>>4 才是实际的数据。而 右对齐,则是数据保持不变,采集到多少就多少

注意:中断原则是快进快出,不要在中断里执行操作,防止重入等问题导致卡死,RTC闹钟中断回调函数就是例子,可以通过设定标志位来执行对应功能

不用的IO口设置为模拟输入态最省电

程序起始时建议延时几秒再进入低功耗模式,否则一进入就不能使用下载口了

DMA那里传输位宽和定义的缓冲区位宽要一致,word–>u32,half word–>u16,byte–>u8

在PWM输出比较时,最好使能 auto-reload preload(自动重装载功能)

PWM输出比较时,在中断回调函数里用__HAL_TIM_GetCounter()函数的话获取的是CNT值,这样算出的频率误差差几百Hz,直接操作寄存器CCRx的话误差不大,所以推荐要么操作寄存器方式要么使用 __HAL_TIM_GetCompare()函数获取CCRx值

还有输出比较的回调函数有2个都可以用,一个是 HAL_TIM_PeriodElapsedHalfCpltCallback,一个是HAL_TIM_OC_DelayElapsedCallback,在HAL_TIM_IRQHandler函数里有写

能用普通模式尽量用普通模式如果题目需要两个频率的话,这样误差小很多,比较输出误差大

当某定时器通道配置为输入捕获时,该通道的CCRx寄存器变为只读,不能去写它,只能在发生捕获时硬件装载修改

输出比较时选定时器时需要注意 32位16位,它们溢出值分别是 429496729565535

G4板子定时器 位数
TIM2 32
TIM1,TIM3,TIM4,TIM6,TIM7,TIM8,TIM15,TIM16,TIM17 16

常规下,使用中断服务函数需要手动清除标志位,使用中断回调函数则不需要手动清除;例如串口,它的调用关系如下:

结构体传参,如果传结构体指针则源地址的数据会被修改,传结构体普通变量则不会修改源地址的数据,所以需要注意一下(->就是指针类型,.就是普通类型)

注意char类型分有符号/无符号;在程序里有符号不一定是char,取决于编译器默认;所以最好使用char有符号时在前面加 signed ;无符号的到0之后会变到最大值255,超出255则变回0

当出现这样的警告:single-precision operand implicitly converted to double-precision 意思是代码中的小数需要转换为双精度,在小数后面加个 f 即可

掉电保护的话,如果遇到小数则可以通过×10/100/1000\times10/100/1000 转换成整数存储在数组里然后存,数组初始化为 ={0},这样,读取时把结果按上面 ÷10/100/100\div10/100/100 恢复原值

串口DMA接收踩坑:默认上电触发一次中断函数的,所以需要在最终数据处理完还要重新打开DMA接收,HAL_UART_Receive_DMA

串口DMA发送踩坑:DMA发送如果太快,会卡死,所以最好不要用DMA发送,使用 HAL_UART_Transmit 发送就正常

当第一次调用HAL_UART_Transmit_DMA()时,串口处于READY状态,于是进行DMA传输,注意,在DMA传输过程中,会将串口从READY状态改为发送BUSY状态(软件修改):huart->gState = HAL_UART_STATE_BUSY_TX;但是在DMA完成本次传输工作以后,并没有将串口从发送BUSY状态改回READY状态

使用DMA发送的话也可以,需要打开DMA发送中断,关闭接收DMA中断,然后需要到 HAL_UART_Transmit_DMA 函数里面修改一下:

解锁操作 __HAL_UNLOCK;后添加代码:
huart -> gState = HAL_UART_STATE_READY;     

接收数据长度是最大长度减去未使用的长度,如果不勾 发送新行,则长度就等于发送的字符个数,勾了 发送新行 数据长度+2(包含\r\n)

如果不想第一次上电中断影响到程序可以这样:

uint8_t One_Open = 0;

//我这个是示例为了第一次中断不触发我的发送函数所以设置了这个++第二次开始就正常了
void USART1_function(void)
{
	if(RX_OVER)
	{
		RX_OVER = 0;
		One_Open++;
		if(One_Open>1)
		{
			if(RX_LEN != 24)
			{
				HAL_UART_Transmit_DMA(&huart1,(uint8_t*)"Error\r\n",sizeof("Error\r\n"));
			}
			else
			{
				LED_DIS(0x01,SET);
			}			
		}

		HAL_UART_Receive_DMA(&huart1,(uint8_t*)RX_BUFF,RX_MAX_LEN);	//开启DMA接收
	}
}

方波(术语)就是 50% 占空比的波形

LCD刷新模板:

uint16_t LCD_State = 0xFFFF;

uint8_t LCD_Line_BUFF[10][20]  ={""};

void LCD_function(void)
{
	for(uint8_t i = 0;i<10;i++)
	{
		if(LCD_State & (1<<i))
		{
			LCD_State &= (~(0x01<<i));
			LCD_DisplayStringLine(i*24,LCD_Line_BUFF[i]);
		}
	}
}
//刷新时可以 LCD_State |= (0x01<<i);i是数组下标
//注意切换屏幕时最后使用清除行函数,把不需要显示的行清除,需要显示的行sprintf。然后对应位置标志位改变不要全部0xFF一起改变不然错乱!!!

//配合数组加空格函数,还有最好sprintf前把数组清空避免出现遗留字符在
memset(LCD_Line_BUFF[C4_STATE],0,sizeof(LCD_Line_BUFF[C4_STATE]));	//清空数组

void Add_Space(uint8_t date[][20],uint8_t State)
{
	for(uint8_t i = 0;i<20;i++)
	{
		if(date[State][i] == '\0')
		{
			date[State][i] = ' ';
		}
	}
}

还有ADC采集,经常题目是主页面显示采集电压,另一个页面不需要显示,这时候ADC采集刷新不能太快不然LED错乱,应该150ms采集一次然后设置一个标志位表示采集完成,在LCD_function()里判断这个标志位是否置1并且是在主页面才刷新否则不刷新

高亮某行的话参考第7届省赛代码

选这个编译速度提升好几倍

PWM输出低电平或者高电平的话不推荐使用直接赋值 100% 或 0%占空比,不稳定,推荐直接引脚改成推挽输出即可,需要的PWM时再重新配置PWM引脚【可在初始化那复制修改】

//引脚输出低电平或者高电平
void GPIO_OUT(uint16_t pin,GPIO_PinState PinState)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};
	GPIO_InitStruct.Pin = pin;	//引脚
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
	HAL_GPIO_WritePin(GPIOA,pin,PinState);	//高、低
}
//引脚输出PWM
void PWM_OC(uint16_t pin)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);	
}

好玩的

工程在:https://github.com/luckys-yang/lan_G4_SS

移植NES模拟器到STM32G431