以蓝桥杯嵌入式开发版G431
下载失败可尝试更改下载速率。
工程创建
CubeMx配置
选择芯片
进去之后点击SystemCore
时钟配置
LCD
#include "lcd.h"
#include "stdio.h"
//界面相关参数
float volt_r37;//电压
u16 freq_pa7 = 1000;//初始值1Khz 输出频率1000Hz
u16 duty_pa7 = 40;//占空比,初始值为40%
//1.LCD
void LCD_Process()
{
u8 display_buf[20];
LCD_DisplayStringLine(Line1,(unsigned char *)" DATA ");
sprintf((char*)display_buf," Volt:%4.2fV",volt_r37);
LCD_DisplayStringLine(Line3, (unsigned char *)display_buf);
sprintf((char*)display_buf," D:%2d%%",duty_pa7);
LCD_DisplayStringLine(Line5, (unsigned char *)display_buf);
sprintf((char*)display_buf," F:%-5dHz",freq_pa7);
LCD_DisplayStringLine(Line7, (unsigned char *)display_buf);
}
注意:display_buf数组最好不要设置为全局变量。
按键
CubeMX配置
1.每次按下按键需要单次触发!
2.需要有松手检测!
3.不阻塞程序!不要有Delay!更不有vhile(!P30):等待按键弹起!
4.最好有长按功能!
无需配置上拉电阻
程序设计
#incldue "key.h"
#define KB1 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)
#define KB2 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)
#define KB3 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)
#define KB4 HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)
#define KEYPORT KB1 | (KB2<<1) | (KB3<<2) | (KB4<<3) | 0xf0
u8 Trg;
u8 Cont;
void Key_Read(void)
{
u8 ReadData = (KEYPORT)^0xff;
Trg = ReadData & (ReadData ^ Cont);
Cont = ReadData;
}
#ifndef __KEY_H
#define __KEY_H
#include "main.h"
extern u8 Trg;
extern u8 Cont;
#endif
//按键
u32 keyTick;
void Key_Process()
{
if(uwTick - keyTick < 20) return;//每20ms执行一次
keyTick = uwTick;
Key_Read();
if(Trg & 0x01) //B1
{
}
if(Trg & 0x02) //B2
{
}
if(Trg & 0x04) //B3
{
}
if(Trg & 0x08) //B4
{
}
}
LED
CubeMX配置
配置PD2(锁存器),为GPIO_output模式
程序思路:
- 上电先让PC8-15输出高电平,关闭全部LED。
- 然后打开锁存器,让PC8-15的数据进去,再关闭锁存器。
- 根据题意点亮对应的LED。设置控制变量led_ctrl。
#ifndef __LED_H
#define __LED_H
#include "main.h"
void LED_Control(u8 led_ctrl);
#endif
#include "led.h"
void LED_Control(u8 led_ctrl)
{
HAL_GPIO_WritePin(GPIOC,0xff00,GPIO_PIN_SET); //让PC8-15输出高电平,熄灭LED
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET); //打开锁存器
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);//关闭锁存器
//根据led_ctrl点亮对应的LED
HAL_GPIO_WritePin(GPIOC,led_ctrl<<8,GPIO_PIN_RESET);//点亮对应的LED
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET); //打开锁存器
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET); //关闭锁存器
}
利用中断精确定时
举例:LED亮3S后熄灭。
1.main函数中
//5.LED
u8 led_ctrl = 0x00;
u32 ledTick = 0;
u16 cnt_led = 0;
void LED_Process()
{
//方法2
if(uart_right_flag == 1)
{
led_ctrl |= 0x01;
}
else
{
led_ctrl &= ~0x01;
}
if(uart_error_flag == 1)
{
led_ctrl |= 0x02;
}
else
{
led_ctrl &= ~0x02;
}
LED_Control(led_ctrl);
}
2.在stm32g4xx_it.c中声明
3.找到SysTick_Handler函数,在内部编写所需要的逻辑
ADC(模拟电压输入)
CubeMX配置
以配置R37(PB15为例)
ADC,模拟电压输入。
最高12位。2^12 = 4096,0-4095
实际电压 = X/4096*3.3。➗4096运行速度比较快。
ADC:显示电压。模拟转数字
DAC:输出电压。数字转模拟
单端模式,差分模式(两个电压做差值)
代码部分
MX_ADC2_Init();//ADCX,X=1,2。CubeMX自动生成
HAL_ADC_Start(&hadc1);//启动
HAL_ADC_GetValue(&hadc1);//获取值
r37单通道采集代码部分:
//ADC
u16 adc2_val;
void ADC_Process()
{
HAL_ADC_Start(&hadc2);
adc2_val = HAL_ADC_GetValue(&hadc2);
volt_r37 = adc2_val / 4096.0f*3.3f;
}
r37,r38双通道采集代码部分:
//ADC
u16 adc1_val,adc2_val;
u32 adcTick;
void ADC_Process()
{
if(uwTick - adcTick < 100) return;//每100ms采集一次
adcTick = uwTick;
HAL_ADC_Start(&hadc1);
adc1_val = HAL_ADC_GetValue(&hadc1);
volt_r38 = adc1_val/4095.0f * 3.3f;
HAL_ADC_Start(&hadc2);
adc2_val = HAL_ADC_GetValue(&hadc2);
volt_r37 = adc2_val/4095.0f * 3.3f;;
}
数字滤波(均值求和)
在ADC中的应用
代码部分
/* ADC */
u16 adc_val;
float volt_r37_prev;
u32 adc_sum;
u8 adc_cnt;
void ADC_Process()
{
HAL_ADC_Start(&hadc2);
adc_val = HAL_ADC_GetValue(&hadc2);
adc_sum = adc_sum + adc_val;
adc_cnt++;
if(adc_cnt == 10)//计算十次平均值
{
volt_r37_prev = volt_r37;//保存上一个时刻的电压
volt_r37 = (adc_sum / 10.0f) / 4095.0f * 3.30f;//获取当前电压值
adc_cnt = 0;
adc_sum = 0;
}
}
DAC输出
CubeMX的配置
选择Connected 同external pin only 模式
其他不用改。
代码部分:
/* DAC */
float dac_volt1,dac_volt2;
void DAC_Process()
{
dac_volt1 = 3.30f;
HAL_DAC_SetValue(&hdac1,DAC_CHANNEL_1,DAC_ALIGN_12B_R,4095/(3.3/dac_volt1));//0-0V,4096-3.3V。默认12位右对齐
HAL_DAC_Start(&hdac1,DAC1_CHANNEL_1);
dac_volt2 = 1.11f;
HAL_DAC_SetValue(&hdac1,DAC_CHANNEL_2,DAC_ALIGN_12B_R,4095/(3.3/dac_volt2));//0-0V,4096-3.3V。默认12位右对齐
HAL_DAC_Start(&hdac1,DAC1_CHANNEL_2);
}
RTC
CubeMX配置
Activate Clock Source使能时钟源 Activate Calendar 使能日历
在时钟为 32KHz的情况下,Asynchronous Predivider value 设置为32-1。Synchronous Predivider value 设置为1000-1 。因为是从0开始计时,0-999为1000。
代码部分
RTC_TimeTypeDef rtc_time;//包含时分秒
RTC_DateTypeDef rtc_date;//包含年月日星期
/* RTC实时时钟 */
void RTC_Process()
{
HAL_RTC_GetTime(&hrtc,&rtc_time,RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc,&rtc_date,RTC_FORMAT_BIN);
}
void LCD_Process()
{
u8 display_buf[20];
switch(display_mode)
{
case 0://界面1
{
sprintf((char*)display_buf," %02d-%02d-%02d ",
rtc_time.Hours,rtc_time.Minutes,rtc_time.Seconds);
LCD_DisplayStringLine(Line9, (uint8_t *)display_buf);
break;
}
}
PWM输出
PWM输出 = 频率输出 = 信号输出
CubeMX配置
其他管脚大同小异,重点掌握PA6,PA7的PWM输出。
PA7的PWM输出
中断不需要配置
参数设置
Prescaler 分频值设置为79(80-1),Counter Period 对应周期(us)。这里设置上电后默认PA7输出1KHz,占空比为20%。对应1000us.(0-999)。
Pulse(us):高电平时间。占空比20%对应为1000us * 20% = 200us
while(1)前
//PWM输出
HAL_TIM_PWM_Start(&htim17,TIM_CHANNEL_1);
完成。编译后下载到板子上,经测试PA7成功输出1000Hz,占空比为20%波形。
PA6的PWM输出
CubeMX的配置
PWM输出不需要开启中断
这里我们目标设置PA6输出10KHz,占空比50%,一般情况下只需修改三个参数。
HAL_TIM_PWM_Start(&htim16,TIM_CHANNEL_1);
自定义函数控制
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);//开启TIM2_CH2的PWM输出
//6.PWM输出。输出方波(占空比为50%)
void PWM_Process()
{
if(volt_r37 > volt_r38)
{
TIM2->ARR = 100-1;//周期为100us(0-99)。10Khz 对应100us.
TIM2->CCR2 = 50;//CCR对应占空比
}
else
{
TIM2->ARR = 5000-1;//周期为5000us(0-4999)。对应200hz
TIM2->CCR2 = 2500;//高电平时间。5000us中2500us为高电平
}
}
CNT为定时器中的计数器,(自动重装载寄存器)ARR寄存器存放的是周期(us),CCR寄存器存放的是高电平时间(us)。
举例:要设置输出1KHz的PWM方波信号(占空比50%),则ARR = 1000-1 = 999.
因为0-999us则为1000ms,对应的频率为1KHz。CCR = 500。1000ms * 50% = 500ms。
PWM捕获
R40对应PA15,R39对应PB4
测量1路频率(PA15 R40)
CubeMX配置
开启中断
参数设置
Prescaler设置为79(0-79 则为80)。时钟80MHz,分频后为1MHz,即1us计时一次。
Counter Period 设置成最大,尽量防止溢出。
代码部分
while(1)前初始化
//PWM捕获
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//开启TIM2_CH1的输入捕获中断
u32 tim2_cnt1 = 0;
u32 f40 = 0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
tim2_cnt1 = __HAL_TIM_GetCounter(&htim2);//获取CNT,单位us
__HAL_TIM_SetCounter(&htim2,0);//设置CNT为0,重新开始计时
f40 = 1e6 / tim2_cnt1;//R40调制的555定时器频率
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//每次都需要重新开启TIM2_CH1的输入捕获中断
}
测量2路频率 (PA15 R40 + PB4 R39)
开启中断
周期最大为65535us。可测量范围为:15Hz-1MHz
参数设置
这里的Counter Period 最大为0xFFFF(65535)
代码部分:
while(1)前:
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);//开启TIM3_CH1的输入捕获中断
/* PWM捕获 */
u32 tim2_cnt1 = 0;
u32 f40 = 0;
u32 tim3_cnt1 = 0;
u32 f39 = 0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
//因为2路PWM捕获利用的是同一个中断函数,所以需要if进行判断
if(htim == &htim2)
{
tim2_cnt1 = __HAL_TIM_GetCounter(&htim2);//获取CNT,单位us
__HAL_TIM_SetCounter(&htim2,0);//设置CNT为0,重新开始计时
f40 = 1e6 / tim2_cnt1;//R40调制的555定时器频率
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//每次都需要重新开启TIM2_CH1的输入捕获中断
}
if(htim == &htim3)
{
tim3_cnt1 = __HAL_TIM_GetCounter(&htim3);//获取CNT,单位us
__HAL_TIM_SetCounter(&htim3,0);//设置CNT为0,重新开始计时
f39 = 1e6 / tim3_cnt1;//R39调制的555定时器频率
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);//每次都需要重新开启TIM3_CH1的输入捕获中断
}
}
如果碰到同一个定时器不同通道,还需对通道进行判断。
测量频率和占空比
基本思路:cnt1为高电平时间,cnt为周期 占空比= cnt1/cnt2。
代码部分:
/* PWM捕获 */
u32 tim2_cnt1 = 0, tim2_cnt2 = 0;//cnt1 为高电平时间 cnt2为周期
u32 f40 = 0;
float d40;
u32 tim3_cnt1 = 0,tim3_cnt2 = 0;//cnt1 为高电平时间 cnt2为周期
u32 f39 = 0;
float d39;
u8 tim2_state = 0;//0-开始计时 1-获取T1 2-获取T2
u8 tim3_state = 0;//0-开始计时 1-获取T1 2-获取T2
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
//因为2路PWM捕获利用的是同一个中断函数,所以需要if进行判断
//tim2_channel 1
if(htim == &htim2)
{
if(tim2_state == 0)//第一个上升沿产生,开始计时
{
__HAL_TIM_SetCounter(&htim2,0);//设置CNT为0,重新开始计时
TIM2->CCER |= 0x02;//CC1P置为1,改为下降沿中断
tim2_state = 1;
}
else if(tim2_state == 1)//获取T1(高电平时间),并改为上升沿中断
{
tim2_cnt1 = __HAL_TIM_GetCounter(&htim2);//获取T1(us)高电平时间
TIM2->CCER &= ~0x02;//CC1P置为0,改为上升沿中断
tim2_state = 2;
}
else if(tim2_state == 2)//第二个上升沿中断,获取T2(us)周期
{
tim2_cnt2 = __HAL_TIM_GetCounter(&htim2);//获取T1,单位us
f40 = 1e6 / tim2_cnt2;//R40调整的555定时器频率
d40 = tim2_cnt1*100.0f/tim2_cnt2;
tim2_state = 0;
}
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//每次都需要重新开启TIM2_CH1的输入捕获中断
}
//tim3_channel 1
if(htim == &htim3)
{
if(tim3_state == 0)//第一个上升沿产生,开始计时
{
__HAL_TIM_SetCounter(&htim3,0);//设置CNT为0,重新开始计时
TIM3->CCER |= 0x02;//CC1P置为1,改为下降沿中断
tim3_state = 1;
}
else if(tim3_state == 1)//获取T1(高电平时间),并改为上升沿中断
{
tim3_cnt1 = __HAL_TIM_GetCounter(&htim3);//获取T1(us)高电平时间
TIM3->CCER &= ~0x02;//CC1P置为0,改为上升沿中断
tim3_state = 2;
}
else if(tim3_state == 2)//第二个上升沿中断,获取T2(us)周期
{
tim3_cnt2 = __HAL_TIM_GetCounter(&htim3);//获取T1,单位us
f39 = 1e6 / tim3_cnt2;//R40调整的555定时器频率
d39 = tim3_cnt1*100.0f/tim3_cnt2;
tim3_state = 0;
}
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);//每次都需要重新开启TIM2_CH1的输入捕获中断
}
}
串口通信
串口配置:
CubeMX配置 PA10和PA9管脚
设置为Asynchronous异步模式,开启串口接收中断。
一般只需将波特率Baud Rate设置为9600 Bits/s。其他保持默认
串口发送:
串口发送方法1:
上电自动发送
//串口发送
//sizeof后面-1,不然只能发送一次。50ms为超时时间
HAL_UART_Transmit(&huart1,(unsigned char *)"Hello DNF!\r\n",sizeof("Hello DNF!\r\n")-1,50);
u8 tx_buf[] = {"DNF,启动!\r\n"};
HAL_UART_Transmit(&huart1,(unsigned char *)tx_buf,sizeof(tx_buf)-1,50);
u8 tx2_buf[] = {"今天又是充满希望的一天。\r\n"};
HAL_UART_Transmit(&huart1,(unsigned char *)tx2_buf,sizeof(tx2_buf)-1,50);
//以上内容上电后自动发送一次
串口发送方法2:
需要重定向printf函数
//串口发送
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1,(unsigned char*)&ch,1,50);
/* Your implementation of fputc(). */
return ch;
}
printf("勇士记得早点起,每日6点更新疲劳值\r\n");
串口成功发送
注意一定要勾选上使用Use MicroLIB,否则printf函数会卡死。
串口接收:
串口接收数据进行实现特定功能程序的基本思路
- 开启中断,定义串口闲时判断函数
- 定义字符串数据检查函数
- 定义串口中断回调函数
开启串口接收中断( while(1)前 )
HAL_UART_Receive_IT(&huart1,uart_buf,1);//开启串口中断
定义串口闲时判断函数,接收数组以及相关变量。
RxIdle_Process():每50ms对串口数据接收数组进行一次清零。
/* 串口 */
u8 uart_buf[2];
u8 rx_buf[10];//串口数据缓存数组
u8 rx_cnt = 0;
u32 uartTick;
u8 rx_cnt_flag = 0;
_Bool rx_cmd_error_flag;//串口接收到错误数据,置1。正确则置0
//每50ms对接收数组进行一次清零
void RxIdle_Process()//串口闲时处理
{
if(uwTick - uartTick < 50) return;//50ms执行一次
uartTick = uwTick;
//--这里根据题目需要修改
if(rx_cnt > 0 && rx_cnt_flag == 0)//接收到数据,但字节数据不足7个
{
printf("Cmd Cnt Error\r\n");
rx_cmd_error_flag = 1;
}
//-------分隔符-------
rx_cnt = 0;
rx_cnt_flag = 0;
memset(rx_buf,'\0',sizeof(rx_buf));
}
定义字符串
判断数据是否符合格式,符合返回1,不符合返回0。
//判断数据是否符合格式,符合返回1,不符合返回0
_Bool Check_String(u8* str)
{
//根据题目需要自行编写
//3.2,1.4
u8 i;
if(str[3] != ',')
return 0;
if(str[1] != '.' || str[5] != '.')
return 0;
for(i=0;i<7;)
{
if(str[i] > '9' || str[i] < '0')
return 0;
i = i + 2;
}
return 1;
}
编写串口接收中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
uartTick = uwTick;
rx_buf[rx_cnt++] = uart_buf[0];//接收数据
HAL_UART_Receive_IT(&huart1,uart_buf,1);//开启下一次串口接收中断
if(rx_cnt == 7)
{
rx_cnt = 0;
rx_cnt_flag = 1;
printf("Cmd Cnt Right\r\n");
if( Check_String(rx_buf) == 1)//数据符合要求
{
/* 根据题目需要自行编写 */
}
else
{
printf("数据格式不符合\r\n");
rx_cmd_error_flag = 1;
}
}
}
同时在while(1)调用RxIdle_Process()函数
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
LCD_Process();
Key_Process();
LED_Process();
ADC_Process();
DAC_Process();
RTC_Process();
RxIdle_Process();
}
数据判断:
串口助手发送数据到单片机
例如发送数字9,发送的是字符9(阿斯康码值57).
所以如果串口助手发送数据到单片机,发送6,想让单片机中的a变量赋值为6,则需要对应接收到的数据进行转换; a = str[0] - '0' 或者 a = str[0] - 48。str[]为串口接收数据数组。
串口DMA模式
CubeMX配置
代码
//串口接收
u8 uart_buf[20];
u8 rx_buf[10];
u8 rx_cnt;
u32 uartTick;
void RxIdle_Process()//串口接收延时判断函数
{
if(uwTick - uartTick < 50) return;//50ms清空一次
uartTick = uwTick;
rx_cnt = 0;
memset(rx_buf,'\0',sizeof(rx_buf));//清空缓存数组
}
/* 串口DMA不定长数据收发 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart == &huart1)
{
HAL_UART_Transmit_DMA(&huart1,uart_buf,Size);
rx_buf[rx_cnt++] = uart_buf[0];
if(rx_cnt > 0)
{
printf("%s",rx_buf);
}
/* 这里使用HAL_UARTEx)ReceiveToldle 或者 HAL_UARTEx_ReceiveToIdle_IT 也可以*/
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,uart_buf,sizeof(uart_buf));//启动接收
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);//关闭DMA传输过半中断
}
}
while(1)前初始化
//串口接收DMA
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,uart_buf,sizeof(uart_buf));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);//关闭DMA传输过半中断
//这里填参数之前,要在main.h 里添加extern DMA_HandleTypeDef hdma_usart1_rx;
EEPROM
代码部分
比赛时需要自己编写的两个函数
//写24C02
void EEPROM_Write(u8 add,u8 dat)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(add);
I2CWaitAck();
I2CSendByte(dat);
I2CWaitAck();
I2CStop();
HAL_Delay(5);
}
//读24C02
u8 EEPROM_Read(u8 add)
{
u8 dat;
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(add);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
dat = I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return(dat);
}
可读写的地址,0x01,0x02,0x03......
while(1)前
I2CInit();
注意事项
全新的EEPROM中的值为255,使用时要进行上电检测。如果EEPROM中读取的参数值超出目标范围,说明是第一次使用,不是我们存储的数据。我们则需要对参数进行初值设置。
简单应用:上电后记录开机次数
//EEPROM,上电后统计开机次数
I2CInit();
startup_times = EEPROM_Read(0x20);
EEPROM_Write(0x20,++startup_times);
关于debug
黄色箭头表示当前所在位置。
蓝色表示光标所在位置。
断点只能打在左侧深灰色处,白色的地方无效。
被优化的变量无法通过watch窗口查看。
局部变量只有运行到所在块内才能查看。
一般程序默认开始位置在main的起始处。
1.跳转到函数里面
2.跳过当前行
3.跳出这行
4.执行到光标所在位置