蓝桥杯嵌入式省赛STM32G431模块总结及配置细节详解

目录

初始配置

led模块

lcd模块

定时器模块

pwm输出模块

频率捕获模块

按键模块

串口模块

adc模块

eeprom模块

 一些细节及总结


初始配置

 打开你的stm32CubeMX,选择File下的Nex Project

 选择板子,创建工程

 配置RCC(深色部分)

 配置SYS

 时钟配置(1,2步没有先后,2步时输入后需要确定,推荐使用170,3步是2步按下enter后才出现的)

 工程管理中工程配置(第1步建议按照省赛题目要求取名,提交以准考证号命名的hex文件,虽然后面可以改hex文件名字)

  工程管理中的代码生成以及生成项目(4步是在3步后出来的,一次生成的时候建议直接打开项目,后面可以直接去keil中打开,然后编译一下,)

 keil中project配置

 keil中配置2

 最后编译一下(第一次工程的时候,可以选1,后面通过stm32cubeMX添加时,选2快一点,记得每次stm32cubeMX改进后需要重新编译一下)

led模块

 介绍:由图可知,led1-led8由PC8-PC15的引脚控制,同时对应的引脚置位低电位,对应的灯才会亮,因为PC8-PC15与lcd的引脚共用了,所以当PD2为高电位时,PC8-PC15作用与led,同理,为低电位时,作用于lcd。因为比赛中lcd需要一直显示,同时肯能需要快速变换数值,而led作用赋值后,对应led会保留赋值状态,所以PD2只有再要改变led是才处于高电位,同时改变完后需要重新置位低电位。

stm32cubeMX中配置,只需要对应引脚输入成GPIO_output

 led代码

uint8_t led_time,led_flag;//全局变量默认值为0
void led_change(uint8_t led_val){
	HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOC,led_val<<8,GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}

/*
    举个例子,打开led1,关闭led2,100ms反转一次led3
*/
void led_prc(){
    uint8_t led_num=0x00;
    led_num|=0x01;
    led_num&=0xfd;
    if(led_flag){
        led_num^=0x04;
        led_flag=0;
    }
    led_change(led_num);
}

//这里拿一个已经弄好的定时器,10ms进入一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	if(htim->Instance==TIM3){
        if(led_flag==0)
            led_time=(led_time+1)%10;
        if(led_time==0&&led_flag==0)
            led_flag=1;
	}
}

//led的初始化,stm32cubeMx配置好后不需要配置了
int main(){
    led_change(0x00);记得全部关闭灯,初始式是全亮的
    while(1){
        led_prc();
    }
}

lcd模块

介绍:官方提供了代码,需要知道其的具体位置,把要用的三个文件放入工程中去。lcd中一般设置为白字黑底,还有就是注意位置和字符串显示到lcd中去

 stm32cubeMX中lcd配置(对应引脚设置为GPIO_Output就行)

 lcd代码

#include "lcd.h"
#include "stdio.h"
#include "string.h"

char str[20];
uint8_t lcd_time;

void lcd_prc(){
    if(lcd_time<30)//有pwm时推荐100ms,没有时推荐300ms
        return ;
    else
        lcd_time=0;
    sprint(str,"  M:%d%%    ",20);//及得多加空格,防止数据长度变换导致数据清理不干净,添加%号
	LCD_DisplayStringLine(Line3,(uint8_t *)str); 
    sprint(str,"  N:%.1f    ",3.21);//保留一位小数
	LCD_DisplayStringLine(Line4,(uint8_t *)str);  
}

//这里拿一个已经弄好的定时器,10ms进入一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	if(htim->Instance==TIM3){
        lcd_time++;
	}
}

int main(){
	LCD_Init();
	LCD_Clear(Black);
	LCD_SetBackColor(Black);
	LCD_SetTextColor(White);
    while(1){
        lcd_prc();
    }
}

定时器模块

介绍:最常用的为用来,进行时间上的把控,所以一般使用打开中断的定时器。时间=1/频率,频率=总频率(80*1e6)/((预分频计数器值+1)*(重置载值+1)),所以时间=((预分频计数器值+1)*(重置载值+1))/(80*1e6),所以设置一个为10ms的定时器,频率应为100Hz,预分频计数器值为Prescaler,重置载值为Counter Period 

 stm32cubeMX配置(第5步去NVIC打开中断)

 定时器代码

//10ms进入一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	if(htim->Instance==TIM3){//用定时器几,就写TIM几

	}
}

ing main(){	
    HAL_TIM_Base_Start_IT(&htim3);//开启定时器中断
    while(1){

    }
}

pwm输出模块

介绍:pwm输出需要通过定时器进行输出,题目一般要求输出一定的频率和占空比,或则更改频率与占空比。由定时器模块我没可以知道频率的具体计算,pwm输出不需要打开定时器中断,占空比=脉冲数(pluse)/重置载值(Counter Period ),更改频率推荐更改重装载值,因为它提供了快速响应和高精度调整的能力,同时对其他定时器功能的影响较小。

 stm32cubeMX配置(1kHz,占空比为20%)

 pwm输出模块代码

int main(){
    HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);
    while(1){
    /*
        更改频率为2KHz,占空比为40%,
        (当然更改频率肯定不能放入while中,要更改的话更改一次就行)
    */
        __HAL_TIM_SetAutoreload(&htim2,500-1);//更改重装载值(Counter Period)
        __HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,500*40/100);//设置脉冲值
        /*
        __HAL_TIM_SET_PRESCALER(&htim2, 40-1);//更改预分频寄存器的数值 Prescaler
        __HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,1000*40/100);//设置脉冲值
        //(建议别这样设置)
        */
    }
}

频率捕获模块

介绍:频率捕获通过定时器通道进行捕获,这个需要开启定时器中断进行捕获,同时预分频计数器的值时固定的,为你设置的主频-1,同时要注意重装载值所能捕获的最大数值,我设置的这个定时器为65535

stm32cubeMX

 频率捕获代码

uint32_t frq;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){//中断读取
	uint32_t val;
	if(htim->Instance==TIM17){
		val=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1)+1;
		__HAL_TIM_SET_COUNTER(htim,0);
		frq=1000000/val;//注意这个当读取3000hz的频率是,会出现问题,这是正常的
		HAL_TIM_IC_Start(htim,TIM_CHANNEL_1);
	}
}

int main(){
    HAL_TIM_IC_Start_IT(&htim17,TIM_CHANNEL_1);
    while(1){
        
    }
}

 板子上的相关模块介绍:可以看到R40与R39是板子4个旋钮中的两个,它们通过旋钮调节可以产生不同频率的输出,可以看到R40产生的通过PA15接收,R39通过PB4接收,当然,你可以把J9拔掉,将2接到我们之前设置的PA7,然后就可以通过lcd显示出具体数只,还能通过旋钮进行调节

按键模块

介绍:按键模块有四个,但有按键按下时,对应的引脚变为低电位,当然为了防止按下抖动,需要进行10ms延时后再判定

 stm32cubeMX(将对应的引脚设置为GPIO_Input)

 按键模块代码:介绍两种,先简单一点的,不用定时是器的,这种也没啥弊端,但处理不了长按键,有人可能说不是直接10ms延时了吗,但这种要有按下才会触发,当然也有可能抖动触发,但10ms也挺短的,当然一直按着就会存在10ms一个while(eeeee)

uint8_t key_read(){
	uint8_t key_val=0;
    if((HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)&HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)&HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)&HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0))==0){
		HAL_Delay(10);//延时10ms
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==0)
			key_val=1;
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)==0)
			key_val=2;
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)==0)
			key_val=3;
		if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0)
			key_val=4;
	}
	return key_val;
}

void key_prc(){
	static uint8_t key_val=0;
	uint8_t key_temp=key_read();//按下的时候,key_temp!=0,同时二值不相等
	if(key_val!=key_temp){//抬起虽然也不相等,当key_temp为0
		key_val=key_temp;
		if(key_val==1){
			
		}
		if(key_val==2){
			
		}
		if(key_val==3){
			
		}
		if(key_val==4){
			
		}
	}
}

int main(){
    while(1){
        key_prc();
    }
}

 再介绍一个定时器中断的方法,每10ms进行一次按键判断,保留之前的按键转态,(好像一想其实还有中断按键判断,还不需要放到while中去,虽然也不好进行长按键判断,但肯定比第一个方法好多了,一直按着也不会再次触发中断,只会在按下的时候触发一次中断,对应的引脚也有,GPIO_EXTI)

uint8_t key_old,key_flag,key_time,key_long_flag,key_long_time_flag;
uint16_t key_long_time;
//配置好的1ms定时器
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	if(htim->Instance==TIM3){
            if(key_flag==0)
                key_time=(key_time+1)%10;
            if(key_time==0&&key_flag==0)
                key_flag=1;
            if(key_long_flag)
                key_long_time=(key_long_time+1)%2000;
            if(key_long_time==0&&key_long_flag){
                key_long_time_flag=1;
            }
                
		}
	}
}

uint8_t key_read(){
	uint8_t key_val=0;
    if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==0)
		key_val=1;
	if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)==0)
		key_val=2;
	if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)==0)
		key_val=3;
	if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0)
		key_val=4;
	return key_val;
}

void key_prc(){
    uint8_t key_up,key_down,key_now;
    key_now=key_read();
    key_down=(key_now)&(key_now^key_old);//(举例一下key1按下)按下瞬间,key_now为1,而key_old为0,则所得值为1,抬起该值为0
    key_up=(~key_now)&(key_now^key_old);//抬起数据,key_now为0,而key_old为1,则所得值为1,按下该值为0
    key_old=key_now;
    if(key_down==1){//按下

    }
    //这里展示一下长按键,2s为例
    if(key_down==2){
        key_long_flag=1;
        key_long_time=0,key_long_time_flag=0;
    }
    if(key_up==2){
        key_long_flag=0;
    }
}

int main(){
    while(1){
        if(key_flag){//10ms进行判断
            key_flag=0;
            key_prc();
            if(key_long_time_flag){//按下时间超过2ms,需要进行的操作可以写到括号里面
                key_long_time_flag=0;
                key_long_flag=0;
            }
        }
    }
}

串口模块

介绍:PA9和PA10与串口1连接,可以与电脑的串口段进行通信

 stm32cubeMX配置(第4步配置注意看题目要求)

 usaet1代码

#include "stdio.h"
#include "string.h"

uint8_t rx_len=0;
char rx_str[30];//这个长度根据题目要求进行更改
uint8_t rx_ch;
char str;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){//接受数据
	if(huart->Instance==USART1){
		rx_str[rx_len]=rx_ch;
		rx_len=(rx_len+1)%30;
		UART_Start_Receive_IT(huart,&rx_ch,1);
	}
}

void Uart1_prc(){
	uint8_t rx_temp_len;
	rx_temp_len=rx_len;
	if(rx_temp_len!=0){
		HAL_Delay(1);//这里最好的是搞个定时器,弄个10ms的,不影响总进程,10ms后再来比较,当然这太麻烦了
		if(rx_temp_len==rx_len){
            if(strcmp(rx_str,"hello")==0){
				sprintf(str,"world");
				HAL_UART_Transmit(&huart1,(uint8_t *)str,strlen(str),50);//发送数据到电脑端
            }
			else{
				sprintf(str,"error");
				HAL_UART_Transmit(&huart1,(uint8_t *)str,strlen(str),50);
			}
			rx_len=0;
			memset(rx_str,0,strlen(rx_str));
		}
	}
}

int main(){
    UART_Start_Receive_IT(&huart1,&rx_ch,1);
    while(1){
        Uart1_prc();
    }
}

adc模块

介绍:对已与板子上的R37与R38旋钮,可以通过旋钮进行电压的调节

 stm32cubeMX(这个选择Adc_INX就行)

 附加(可以把ADC2_IN15的这个加号也点了,ADC1_IN11也有)

 adc读取代码

float adc_value1,adc_value2;
float Adc_value(ADC_HandleTypeDef *hadc){
	uint16_t value=0;
	HAL_ADC_Start(hadc);
	value=HAL_ADC_GetValue(hadc);
	return value*3.3/4095;
}

int main(){
    while(1){
		adc_value1=Adc_value(&hadc2);
		adc_value2=Adc_value(&hadc1);
    }
}

eeprom模块

介绍:M24c02就是eeprom,总共256个字节可以用来存储地址,同时每也有8个地址,也就是这个有32页,MCP4017T一个可编程电阻

 配置:这个官方也提供了iic的代码(把i2c_hal.c与.h引用到工程中去)

 stm32cubeMX配置(设置位GPIO_Output就行,应该是要搞个输入的,应该是提供的代码中会变换吧)

 eeprom代码:这里提供两种读和写,一种是单独读取一个地址和写入一个地址,还有一个种是连续的读和写,为啥要弄连续的呢,因为当你连续使用单个写入时,需要中间延时10ms(我建议),否则连续写入会出错,但是你是单个的连续读是不需要延时的(读取过程可以立即开始,并且数据可以在发送读取命令后立即从EEPROM获得),所以呢,连续读其实也没啥用,对了,还需要注意连续读和写的限制,其只能在同一页内进行,也就是最多连续8个字节(一个字节8位),顺便介绍一下应答和非应答

应答(ACK): 当从设备成功接收到一个字节数据后,它会回应一个ACK信号,表示数据已被成功接收并且设备准备好接收下一个字节。
非应答(NACK): 相反,NACK信号用于指示不接受数据或通信结束。发送NACK可以是因为接收到的数据有误,或者在读操作中,主设备通过发送NACK来通知从设备最后一个字节已被接收,接下来将结束通信。

总结:eeprom可以进行多字节存储,它一页有八个字节,所以最多可以进行八个字节的读写,读时需要注意继续读需要发送应答信号,不继续读时需要发送非应答信号。同时可以进行不同数据的读写,double一个数据8个字节,或者进行结构体或者数组的读写

#include "i2c_hal.h"
uint8_t arr[6];
void Read_eeprom(uint8_t addr,uint8_t *data,uint8_t num){//连续读
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(addr);
	I2CWaitAck();
	I2CStop();
	
	I2CStart();
	I2CSendByte(0xa1);
	I2CWaitAck();
	while(num--){
		*data++=I2CReceiveByte();
		if(num)
			I2CSendAck();
		else
			I2CSendNotAck();
	}
	I2CStop();
}

uint8_t Read_e2prom(uint8_t addr){//单独读取一个
	uint8_t data;
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(addr);
	I2CWaitAck();
	I2CStop();
	
	I2CStart();
	I2CSendByte(0xa1);
	I2CWaitAck();
	data=I2CReceiveByte();
	I2CSendNotAck();
	I2CStop();
	return data;
}

void Write_e2prom(uint8_t addr,uint8_t data){//单独写
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(addr);
	I2CWaitAck();
	I2CSendByte(data);
	I2CWaitAck();
	I2CStop();
}

void Write_eeprom(uint8_t addr,uint8_t *data,uint8_t num){//连续写
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(addr);
	I2CWaitAck();
	while(num--){
		I2CSendByte(*data++);
		I2CWaitAck();
	}
	I2CStop();
}

int main(){
    /*
        有的题目喜欢这样出,我第一次需要写入一个数据,但第二次的时候我需要取都这个数据,这个只要去读一个不需要用的地址,然后随便确定一个数,当然最好选两个地址,举例地址0第一次写入10,后面需要读取
    */
    if(Read_e2prom(0x06)==56&&Read_e2prom(0x07)==76){
        arr[0]=56,arr[1]=76;
        Write_eeprom(0x06,arr,2);
        arr[0]=10;
        Write_eeprom(0x00,arr,1);
    }
    else{
        arr[0]=Read_e2prom(0x00);
    }
    //当然eeprom肯定不能翻入while中,我这就演示一下
    while(1){
    	arr[0]=10,arr[1]=10,arr[2]=10,arr[3]=10;
		arr[4]=65,arr[5]=56;
		Write_eeprom(0x00,arr,sizeof arr);//sizeof是将总字节数除以8位,所以如果是uin16_t娜美就是双倍了
        HAL_Delay(10);//写函数直接需要10ms延时
        Write_e2prom(0x06,12);
        Read_eeprom(0x01,arr,sizeof arr);//这其实地址为0x01,则读取总字节数不能超过7
        arr[5]=Read_e2prom(0x00);
    }
}

 一些细节及总结

函数调用放到main之外
输入频率设置为24,最外面那个
注意设置优先级(这里得搞个图)


float和double的数值等于某个值,最好是进行大小比较,如fabs(a-1.0<1e-6或1e-9)(0.000001),同时需要取绝对值,及得加头文件#include "math.h"
循环中i没赋初始值,+1没效果
去.h列表中找不到相应的.h,可能是没有编译adc的读取需要先乘在除单精度运算隐式转换成双精度运算了,在浮点数字后面加上f,编译警告就会消失。
V = (f*2*R*3.14f)/(100*K);
keil中的换行是/r/n

讲下我总结的串口数据读取
sscanf(rx_data, "%9[^:]:%9[^:]:%2d%2d%2d%2d%2d%2d", …);
这样可以确保即使输入字符串过长,也不会导致缓冲区溢出。在 %9[^:] 中,9 表示最多读取 9 个字符(第 10 个位置留给字符串结束符 '\0')。

作者:abc123sdc

物联沃分享整理
物联沃-IOTWORD物联网 » 蓝桥杯嵌入式省赛STM32G431模块总结及配置细节详解

发表评论