目录
一、cubemx时钟树配置
二、LED
三、按键
四、LCD
五、串口
六、定时器
一、cubemx时钟树配置
(1)
(2)
时钟树的配置方法不单图中所示,可以通过调节第4、第5步的地方保证SYSCLK(MHz)处的频率为80,符合蓝桥杯题目要求即可,如果题目要求其他频率也可通过这些地方调整达到题目要求。
二、LED
将PC8~PC15、PD2在cubemx上设置为GPIO_Output模式
(1)一个函数控制所有灯
void Led_Disp(uint8_t ucled)
{
HAL_GPIO_WritePin(GPIOC,0xFF<<8,GPIO_PIN_SET);//指定所有灯灭
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC,ucled<<8,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
该函数的参数是一个字节,0x00 = 0000 0000,每个位代表一个LED灯,从左往右分别代表LED1-LED8
LED1 : 0x01
LED2 : 0x02
LED3 : 0x04
LED4 : 0x08
LED5 : 0x10
LED6 : 0x20
LED7 : 0x40
LED8 : 0x80
流水灯演示:
uint8_t leds[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
while (1)
{
uint i;
for(i=0;i<8;i++)
{
Led_Disp(leds[i]);
HAL_Delay(100);
}
}
(2)一个函数只控制一个灯
uint8_t led_sta;
void Led_Pro(uint8_t num,uint8_t sta)//单独开关灯函数
{
uint8_t pos=0x01<<(num-1);
led_sta=(led_sta&(~pos))|(pos*sta);
Led_Disp(led_sta);
}
灯演示
Led_Pro(6,1);//打开led6
Led_Pro(6,1);//关闭led6
三、按键
将PB0、PB1、PB2、PA0在cubemx上配置为GPIO_Input模式
如果按键想用外部中断的话,需要注意PA0、PB0是同一个中断线,外部中断只能用三个按键,剩下的一个按键还是需要读写操作,所以最好还是都使用读写操作吧。
(1)按键初始化代码
uint8_t Key_Scan(void)
{
uint8_t key_val=0;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==GPIO_PIN_RESET)
{
key_val = 1;
}
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)==GPIO_PIN_RESET)
{
key_val = 2;
}
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)==GPIO_PIN_RESET)
{
key_val = 3;
}
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_RESET)
{
key_val = 4;
}
return key_val;
}
(2)按键长按、短按模板
注意:这个代码中不可以将if分支语句改为switch分支语句,两者理论上看似无差别,但实际操作中存在区别。
if分支在实际操作时,当长按800ms后自动切换操作。
switch分支实际操作时,需要按下超过800ms后松开按键才能切换操作,if则不需要
void Key_Proc(void)
{
key_val = Key_Scan();
key_down = key_val & (key_val^key_old);
key_up =~ key_val & (key_val^key_old);
key_old = key_val;
if(key_down)
{
uskey = 0;
}
if(uskey<800)
{
if(key_up==1)//短按
{
//按下后需要做的事情
ucled=0x01;
}
if(key_up==2)//短按
{
}
if(key_up==3)//短按
{
}
if(key_up==4)//短按
{
}
}
else
{
if(key_val==1)//长按
{
ucled=0x02;
}
if(key_val==2)//长按
{
}
if(key_val==3)//长按
{
}
if(key_val==4)//长按
{
}
}
}
注意:定义:uint16_t uskey;
同时uskey需要放到stm32g4xx_it.c文件的void SysTick_Handler(void)中,如下图。
(3)按键单、双击模板
逻辑:
当按键抬起时,开始计时。
双击:当在规定时间内再次按下该按键。
单击:超时没有按键按下。
void Key_Proc(void)
{
key_val = Key_Scan();
key_down = key_val & (key_val^key_old);
key_up =~ key_val & (key_val^key_old);
key_old = key_val;
if(key_up)
{
key_temp=key_up;
if(key_flag==0)
{
uskey=0;
key_flag=1;
}
else
{
key_flag = 0;//双击结束后的下一次可以开始计时
}
}
if(key_flag==1)
{
if(uskey<300)
{
if(key_down==1&&key_temp==1)//双击
{
ucled=0x02;
}
if(key_down==2&&key_temp==2)//双击
{
}
if(key_down==3&&key_temp==3)//双击
{
}
if(key_down==4&&key_temp==4)//双击
{
}
}
else
{
if(key_temp==1)//单击
{
ucled=0x01;
}
if(key_temp==2)//单击
{
}
if(key_temp==3)//单击
{
}
if(key_temp==4)//单击
{
}
key_flag=0;
}
}
}
四、LCD
在官方提供的资源包中找到fonts.h 、lcd.h、lcd.c文件加到自己的工程文件中即可。
注意:如果是使用操作系统的朋友需要将lcd.c文件中的所有HAL_Delay函数替换成操作系统对应的延时函数
(1)LCD与LED引脚冲突
在lcd.c中找到
LCD_WriteReg();
LCDWriteRAM_Prepare();
LCD_WriteRAM();
三个函数的首尾添加这两句,也就是先把GPIOC- >ODR的数据先存起来等LCD操作完后再把存起来的数据放回ODR寄存器。
u16 pcout = GPIOC->ODR;
/*
三个函数中的原代码最上和最下面加上这两段代码
*/
GPIOC->ODR = pcout;
五、ADC采样
double getADC(ADC_HandleTypeDef *pin)
{
unsigned int adc;
HAL_ADC_Start(pin);//HAL库内置函数,开启ADC
adc = HAL_ADC_GetValue(pin);//HAL库内置函数,得到ADC的值
return adc*3.3/4096;//测量的值均分4096份,看在3.3(电源电压占的比例)因为是12位精度所以除以4096
}
下面这行放主函数中
adc_value2 = getADC(&hadc2);
sprintf(text,"ADC2:%.2f",adc_value2);
LCD_DisplayStringLine(Line4,(uint8_t*)text);
六、串口
注意:串口的cubemx配置一定要严格按照上面的顺序操作,不然容易USART_RX和USART_TX会自动配置载PC4和PC5,忘记收到改回来。最后造成串口不发送也不接收。
根据题目要求注意在parameter Settings中改波特率,这里就不展示了
案例:每次上电后发送Hello World!
sprintf(tx_buf,"hello world!\r\n");
HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50);
(1)接收单个字符
案例:接收到0时,点亮LED1并发送LED1 Open!,接收到C时,熄灭LED1并发送LED1 Close!
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
if(rx_data=='X')
{
LCD_Clear(Black);
LCD_DisplayStringLine(Line4," UART TEST");
sprintf(temp,"X\r\n");
HAL_UART_Transmit(&huart1,(uint8_t*)temp,strlen(temp),50);
}
else
{
LCD_Clear(Black);
LCD_DisplayStringLine(Line4," ERROR");
sprintf(temp,"other\r\n");
HAL_UART_Transmit(&huart1,(uint8_t*)temp,strlen(temp),50);
}
}
HAL_UART_Receive_IT(&huart1,&rx_data,1);
}
注意:需要将HAL_UART_Receive_IT(&huart1,&rx_data,1)放到主函数中才能触发中断
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)在stm32g4xx_hal_uart.h里面,在1632行的位置(如果记不住,就记1600多行的位置)
(2)接收长字符串
void Uart_Proc()
{
if(rx_count==4)
{
if(strcmp(rx_buf,"LED0")==0)
{
ucled = 0x01;
sprintf(tx_buf,"LED1 Open!\r\n");
HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50);
}
if(strcmp(rx_buf,"LEDC")==0)
{
ucled = 0x00;
sprintf(tx_buf,"LED1 Close!\r\n");
HAL_UART_Transmit(&huart1,(uint8_t*)tx_buf,strlen(tx_buf),50);
}
}
rx_count = 0;
memset(rx_buf,0,20);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
rx_buf[rx_count++] = rx_data;
HAL_UART_Receive_IT(&huart1,&rx_data,1);
}
注意:串口显示发送的数据时,可以在HAL_UART_Transmit函数之前延时10-20ms
例如:
if(rx_count != 0)
{
HAL_Delay(20);
HAL_UART_Transmit(&huart1,(u8*)rx_buf,strlen(rx_buf),100);
if(rx_buf[0]==password[0]&&rx_buf[1]==password[1]&&rx_buf[2]==password[2]&&rx_buf[3]=='-')
{
password[0]=rx_buf[4];
password[1]=rx_buf[5];
password[2]=rx_buf[6];
}
}
rx_count = 0;
memset(rx_buf,0,20);
补充:解决接收不完整,接收错误指令问题
补充:调用Uart_Proc()时,可以增加延时,解决接收不完整问题。
if(rx_count != 0)
{
uint8_t temp = rx_count;
HAL_Delay(1);//1ms后,若任在进入中断,xx_count改变,说明接收未完成
if(temp==rx_count) Uart_Pro();
}
七、定时器
(1)定时器定时
向上计数模式:计数器从0计数到自动加载值(TIMx_ARR),然后回到0重新开始向上计数
向下计数模式:计数器从自动加载值(TIMx_ARR)开始向下计数到0,再回到自动加载值重新开始向下计数。
中央对齐模式:计数器从0开始计数到自动载入的值-1,产生一个计数器溢出事件,然后向下计数 到1产生一个计数器溢出事件,然后从0开始重新计数。
定时器周期计算公式:T = (arr + 1)*(PSC+1)/Tck其中Tck为时钟频率,PSC为时钟预分频系数,arr为自动重载值,例如如果Tck=80MHz,PSC=80-1,arr=1000-1,则T=1ms,即中断开启的话1ms进入一次中断。
主函数调用此函数打开定时器
HAL_TIM_Base_Start_IT(&htim6);
将定时的代码写在stm32g4xxit.c中的TIM6DAC_IRQHandler(void)
第二种是使用定时器的中断回调函数进行定时
(2)脉冲信号输出
下面这串代码放在主函数中用于开启pwm输出
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_2);//pwm输出开启
改变重装载值
__HAL_TIM_SetAutoreload(&htim3,1000000/pa7-1);//改变Counter Period (AutoReload Register)的值来改变频率
TIM3->CNT = 0;//每次改变都要置0一次
例:系统为80MHz,pa7=1000,配置的预分频值为80-1,那么重装载值就为1000000/1000-1=1000-1。频率f=80000000/1000/80=1000。
改变占空比
__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_2,(1000000/pa7+1)*pwm/100);//第三个参数除以Counter Period (AutoReload Register)的值就是占空比
__HAL_TIM_SetCompare的第三个参数除以Counter Period的值就是占空比。
(3)定时器输入捕获
主函数中开启输入捕获
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
中断函数调用
//PWM捕获中断的回调函数
double cap1,cap2,R39_frq,R40_frq;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM2)
{
cap1 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
TIM2->CNT=0;
R40_frq = 80000000/80/cap1;
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);
}
if(htim->Instance==TIM3)
{
cap2 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
TIM3->CNT=0;
R39_frq = 80000000/80/cap2;
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
}
}
占空比测量
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
duty1 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
cap = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
TIM3->CNT = 0;
frq = 1000000/cap;
P = (1.0*cap)/(1.0*duty1)*100;
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_2);
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
}
}
}
八、eeprom读写
eeprom掉电保存数据需要用到I2C
注意:官方提供了i2c_hal.c文件,需要自己手动添加到自己的工程文件中
将以下代码封装在i2c_hal.c文件中
void eeprom_write(uint8_t addr,uint8_t dat)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CSendByte(dat);
I2CWaitAck();
I2CStop();
HAL_Delay(20);
}
uint8_t eeprom_read(uint8_t addr)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
uint8_t dat = I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return dat;
}
在主函数数中进行读操作
uint8_t dat=eeprom_read(0);//读0地址上的值
char temp[20];
sprintf(temp," %d ",dat);
LCD_DisplayStringLine(Line0,(u8*)temp);
写操作
eeprom_write(0,10)//在片区0地址写入10
九、rtc实时时钟
注意:在开始写之前需要对rtc进行移植,在main.c中滑到最下找到void SystemClock_Config(void)
在这个函数中添加以下代码即可完成移植
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC|RCC_PERIPHCLK_ADC12;
PeriphClkInit.Adc12ClockSelection = RCC_ADC12CLKSOURCE_SYSCLK;
PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
首先先在main.c中定义下面两个全局变量结构
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};
然后主函数中调用
HAL_RTC_GetTime(&hrtc,&sTime,RTC_FORMAT_BIN);//获取时间
HAL_RTC_GetDate(&hrtc,&sDate,RTC_FORMAT_BIN);//获取日期
注意:即使你不需要获取日期,也要将获取日期的函数添上,不然的话时间就不会流动
最后显示
sprintf(temp," %2d:%2d:%2d ",sTime.Hours,sTime.Minutes,sTime.Seconds);
LCD_DisplayStringLine(Line4,(u8*)temp);
sprintf(temp," %d-%d-%d-%d",sDate.Year,sDate.Month,sDate.Date,sDate.WeekDay);
LCD_DisplayStringLine(Line5,(u8*)temp);