1. 准备工作
(1)安装STM32CubeMX
Step 1 :官网下载地址:https://www.st.com/en/development-tools/stm32cubemx.html
Step2 : 点击"Get Software" --> “STM32CubeMX-Win”(根据系统选择对应的) --> “Get latest” --> “Accept” --> 个人信息随意填一下
(2) 安装STM32G431芯片包
在上一篇【备战蓝桥杯】中有写如何下载对应的芯片包,下载好后是一个‘.pack’的文件,双击自动安装即可。再次打开keil5,会提示设备已经发生改变(devices has been modified类似的),确定即可更新完成。
(3) 将比赛提供的lcd驱动代码复制到自己的工程里
‘Src’里的‘lcd.c’、‘Inc’里的‘lcd.h’‘fonts.h’。
(4) 查找往年真题
到蓝桥杯官网,搜索框中‘真题’,第一个就是。
↓↓↓↓↓↓↓↓↓↓↓↓ 下面直接对照【第十四届省赛真题】来一步步实现功能 ↓↓↓↓↓↓↓↓↓↓↓↓
2. 配置32CubeMX
共包含以下几个部分:Pinout & Configuration, Clock Configuration, Project Manager。
(1)引脚、时钟、定时器、ADC等配置
-
按键
题目中要求,B1, B2, B3, B4 四个按键要被使用到。通过查询《CT117E-M4产品手册》原理图可知,Bx(x=1 … 4) 对应着 PB0, PB1, PB2, PA0。所以:
左键单击这四个引脚,选择‘GPIO_Input’,设置为输入模式。 -
定时器
题目中,通过PA1引脚输出PWM信号。PWM波有两个参数:频率和占空比。我们想调节一个周期内高电平、低电平的占比,这就需要用到定时器来计时了。
注意了,‘CHxN’这类字样的不可选。随后来到左侧下拉栏,选择‘Timers’–‘TIM2’
按照上图,将通道2选为PWM生成,然后设置预分频系数和重装载值。系统时钟是80MHz,我们想生成 xHz 的信号,需要使得80 000 000 / (预分频系数+1) / 自动重装载值 = Frequency,如图的系数设置最终计算得信号频率是4KHz,也就是题目要求的低频信号,如果要生成高频信号(8KHz),后续在代码中将重装载值修改为125即可。 -
ADC
题目中3.1,3)要求“通过ADC功能检测R37上的模拟输出电压”,电位器R37拧一圈其实就是通过调节电阻的大小来改变分压。观察原理图,PB15.
将PB15勾选为ADC2_IN15,左侧把IN15 Single-ended勾一下,其他不用管。
另外,题目中要求PA7捕获输入脉冲,所以也将PA7引脚勾选为TIM2_CH2,同时左侧选择该通道为‘Input Capture direct mode’。
-
其他
最后剩下的就是LED了,它们对应了PC8,…15引脚,将他们勾选为‘Output’输出类型,就不放图演示了。
注意,PD2也要配置为输出模式。(将LED的这八个引脚初始化为高电平,作为熄灭初始化)
(2) 配置时钟树
按照如图修改,可以看到最右侧的系统时钟频率是80MHz。
(3) 工程管理设置
最后一步,如何更改第2步的路径,在上一篇中有讲到。
最后,可以点击‘Generate Code’啦~
3. 书写逻辑代码
(1) 再啰嗦两句
‘GENERATE CODE’之后的代码如上图,我们写代码的时候一定要在某个模块的BEGIN和END之间来写,这样才能保证再次修改32Cube MX配置时自己的代码不会被覆盖掉。
上图是自动补全功能的开启(一般默认开启)。
在参考别人代码时,经常不懂某个函数的意义,可以如上图一样转到定义查看注释。
(2)整体逻辑的思路
首先,为了实现整体的功能,我们需要在main.h里的while(1){}里面书写全部逻辑,已达到轮询的目的。
任务整体分为四个部分:按键模块 keyFunction()、LED模块 LEDwork()、LCD显示模块 LCD_Display()、数据处理模块 dataProcess()。
下面分别实现每个模块。
(3)按键模块
/*******************************************************************************
* Function Name : keyFunction
* Description : 管理按键.
* Input : None
* Output : None
* Return : None
* Source location : myFunction.c
*******************************************************************************/
void keyFunction(void)
{
KeyScan();
if(KeyFalling == Key_B4)
{
KeyTick = HAL_GetTick(); // 记录B4按下的时刻
}
switch(KeyRising)
{
case Key_B1:
mod++;
// 界面从‘数据界面’切换到‘参数界面’
if(mod == 1) RKcount = 0; // 每次从数据界面进入参数界面,默认调整R参数
// 有两种情况:(1)界面由‘参数’到‘统计’;(2)界面由‘统计’到‘数据’
if(mod != 1)
{
for(int i = 0; i <= 1; i++)
{
if(RKtemp[i] != RKvalue[i]) RKvalue[i] = RKtemp[i]; // 如果符合情况(1),则更新R, K
}
}
// 形成环路,回到数据界面
if(mod == 3) mod = 0;
break;
case Key_B2:
// 数据界面
if(mod == 0 && LED2Flag == 0 && sysCount[0] >= 5000)
{
sysCount[0] = 0;
LED2Flag = 1;
}
// 参数界面
if(mod == 1)
{
RKcount ^= 1; // 取反,切换R、K参数
}
break;
case Key_B3:
// 参数界面
if(mod == 1)
{
RKtemp[RKcount]++; // 改变的是中间值RKtemp,因为要等B1按下,参数才会更新,所以要把值丢给RKtemp暂时保存一下
if(RKtemp[RKcount] > 10) RKtemp[RKcount] = 1;
}
break;
case Key_B4:
// 数据界面
if(mod == 0)
{
if(HAL_GetTick() - KeyTick > 2000) // B4按下到松开超过2s,lock
{
lock = 1;
}
else // 姑且认为,只要按住B4的时间小于2s就算‘短按键’
{
lock = 0;
}
}
// 参数界面
if(mod == 1)
{
RKtemp[RKcount]--;
if(RKtemp[RKcount] < 1) RKtemp[RKcount] = 10;
}
break;
default:
break;
}
// 5s之后更新频率模式标志
if(LED2Flag && sysCount[0] >= 5000)
{
modeFreqCount ^= 1;
modeFreqSwitchCount++;
LED2Flag = 0;
}
}
以上是按键模块,模块中出现的变量均在myFunction.c 的头部定义为了全局变量,**详细代码需查看工程原文件。**在函数的头注释中,特意加了一行名为‘Source location’,以说明此段代码的位置。
#include "stm32g4xx_hal.h"
#include "key.h"
#include "stm32g4xx.h" // Device header
#include "gpio.h"
/* 定义初始变量 */
uint8_t KeyOldState = 0;
uint8_t KeyFalling = 0;
uint8_t KeyRising = 0;
/*******************************************************************************
* Function Name : KeyScan
* Description : 扫描按键
* Input : None
* Output : KeyFalling 表示某个按键按下;KeyRising 表示某个按键松开
* Return : None
* Source location : key.c
*******************************************************************************/
void KeyScan(void)
{
uint8_t state = getKeysState();
uint8_t key_temp = 0xFF ^ (0xF0 | state); // 异或操作,对state按位取反
/* 通过逻辑运算实现消抖 */
// 按下状态
KeyFalling = key_temp & (key_temp ^ KeyOldState); // 某一位由0变为1,表示对应的按键按下
// 松开状态
KeyRising = ~key_temp & (key_temp ^ KeyOldState);
// 保存本次按键的值
KeyOldState = key_temp;
}
/*******************************************************************************
* Description : getKeysState()的宏定义
* Source location : key.h
*******************************************************************************/
// 四位按键状态,高位到地位分别表示按键B1, B2, B3, B4
#define getKeysState() ( HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) << 0 | HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) << 1 | \
HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) << 2 | HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) << 3 )
(4) 数据处理模块
/*******************************************************************************
* Function Name : getADC
* Description : 将adc采集到的输入模拟信号转为数字信号.
* Input : adc
* Output : 电压值
* Return : 电压值
* Source location : myFunction.c
*******************************************************************************/
double getADC(ADC_HandleTypeDef *adc)
{
HAL_ADC_Start(adc);
valueADC = HAL_ADC_GetValue(adc);
return (valueADC * 3.3 / 4096); // 4096 = 2^12
//return valueADC;
}
/*******************************************************************************
* Function Name : convert_frequencyTOv
* Description : 频率f转换为速度值v.
* Input : 整型变量freq
* Output : 浮点数v
* Return : v
*******************************************************************************/
float convert_frequencyTOv(int freq)
{
extern uint8_t R, K;
return (freq * 2 * 3.14 * RKvalue[0]) / (100 * RKvalue[1]);
}
/*******************************************************************************
* Function Name : getDuty
* Description : 将电压值转化为占空比
* Input : 电压值
* Output : 占空比
* Return : 占空比
*******************************************************************************/
char getDuty(double voltage)
{
if(voltage >= 0 && voltage < 1) return 10;
else if(voltage >= 3) return 85;
else return (char)(75 / 2 * voltage + 10 - 75 / 2); // 电压-占空比 线性转换
}
/*******************************************************************************
* Function Name : dataProcess
* Description : 数据处理逻辑函数.
* Input : None
* Output : None
* Return : None
*******************************************************************************/
static void dataProcess(void)
{
/* ADC获取R37调节的占空比 */
if(lock == 0)
{
adcV = getADC(&hadc2); // 获取电压值
duty = getDuty(adcV);
if(dutyTemp != duty && LED2Flag == 0) // 设置PA1输出的占空比
{
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_2, PA1freq[modeFreqCount]* (duty / 100)); //
dutyTemp = duty;
}
}
// 速度v
if(fTemp != f)
{
v = convert_frequencyTOv(f);
fTemp = f;
sysCount[2] = 0;
}
else if(f == fTemp && sysCount[2] >= 2000) // 速度保持超过2s,考虑更新vmax
{
if(modeFreqCount == 0)
{
if(v > vLowFreq) vLowFreq = v;
}
if(modeFreqCount == 1)
{
if(v > vHighFreq) vHighFreq = v;
}
}
}
(5) LED模块
/*******************************************************************************
* Function Name : LEDwork
* Description : LED工作函数.
* Input : None
* Output : None
* Return : None
* Source location : myFunction.c
*******************************************************************************/
void LEDwork(void)
{
// 数据界面,LED1亮
if(mod == 0)
{
LED_On(LED1);
}
else
{
LED_On(LEDall);
}
//切换期间,LED2闪烁,间隔0.1s
if(LED2Flag && sysCount[1] >= 100)
{
switchLED(LED2);
sysCount[1] = 0;
}
else if(!LED2Flag)
{
singleLED(LED_2, 0);
}
// 锁定模式下,LED3亮
if(lock == 1)
{
singleLED(LED_3, 1);
}
else
{
singleLED(LED_3, 0);
}
}
/*******************************************************************************
* Function Name : LED_On
* Description : 控制所有LED的状态
* Input : LEDx x=1..8(宏定义:LED1 = 0x01)
* Output : None
* Return : None
* Source location : led.c
*******************************************************************************/
void LED_On(uint16_t dsLED)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_All, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC, (dsLED << 8), GPIO_PIN_RESET); // 低电平点亮
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET); // 打开锁存器,打开的这一瞬间数据就传过去了
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET); // 关闭锁存器,防止误写
}
/*******************************************************************************
* Function Name : switchLED
* Description : 翻转LED的电平
* Input : LEDx x=1..8(宏定义:LED1 = 0x01)
* Output : None
* Return : None
* Source location : led.c
*******************************************************************************/
void switchLED(uint16_t dsLED)
{
HAL_GPIO_TogglePin(GPIOC, (dsLED << 8));
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
/*******************************************************************************
* Function Name : singleLED
* Description : 改变单个LED的状态
* Input : 枚举类型选择LED
LEDx, x : 1..8
* Input : LED状态
1 : ON ; 0 : OFF
* Output : None
* Return : None
* Source location : led.c
*******************************************************************************/
void singleLED(enum LEDLOCATION LEDlocation,char LEDSTATE)
{
HAL_GPIO_WritePin(GPIOC, LEDlocation, (LEDSTATE == 1 ? GPIO_PIN_RESET : GPIO_PIN_SET));
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
/* Source location : led.h */
enum LEDLOCATION { // 枚举
LED_1 = GPIO_PIN_8,
LED_2 = GPIO_PIN_9,
LED_3 = GPIO_PIN_10,
LED_4 = GPIO_PIN_11,
LED_5 = GPIO_PIN_12,
LED_6 = GPIO_PIN_13,
LED_7 = GPIO_PIN_14,
LED_8 = GPIO_PIN_15};
(6) LCD显示模块
/*******************************************************************************
* Function Name : LCD_Display
* Description : 控制LCD屏幕显示.
* Input : None
* Output : None
* Return : None
* Source location : myFunction,c
*******************************************************************************/
static void LCD_Display(void)
{
char temp[20];
if(mod == 0) // 数据界面
{
LCD_DisplayStringLine(Line1, (u8*)" DATA ");
sprintf(temp, " M = %c ", modeFreq[modeFreqCount]);
LCD_DisplayStringLine(Line3, (u8*)temp);
sprintf(temp, " P = %d%% ", duty);
LCD_DisplayStringLine(Line4, (u8*)temp);
sprintf(temp, " V = %.1f ", v); // v保留1位小数
LCD_DisplayStringLine(Line5, (u8*)temp);
}
else if(mod == 1) // 参数界面
{
LCD_DisplayStringLine(Line1, (u8*)" PARA ");
sprintf(temp, " R = %d ", RKvalue[0]);
LCD_DisplayStringLine(Line3, (u8*)temp);
sprintf(temp, " K = %d ", RKvalue[1]);
LCD_DisplayStringLine(Line4, (u8*)temp);
LCD_ClearLine(Line5);
}
else if(mod == 2) // 统计界面
{
LCD_DisplayStringLine(Line1, (u8*)" RECD ");
sprintf(temp, " N = %d ", modeFreqSwitchCount);
LCD_DisplayStringLine(Line3, (u8*)temp);
sprintf(temp, " MH = %.1f ", vHighFreq);
LCD_DisplayStringLine(Line4, (u8*)temp);
sprintf(temp, " ML = %.1f ", vLowFreq);
LCD_DisplayStringLine(Line5, (u8*)temp);
}
}
(7)PA7输入捕获(timer3定时器)
/* 定时器回调函数,无需外部调用 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3) // 条件判断。确保这是与定时器TIM3相关的事件。
{
cclValue = __HAL_TIM_GET_COUNTER(&htim3); // 统计两次输入捕获事件之间的计数值差
__HAL_TIM_SetCounter(&htim3, 0); // 计数器清零
f = (80000000 / 80) / cclValue; // 80MHz,80为预分频系数. 计算输入频率f
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2); // 重新启动TIM3-通道2的输入捕获
}
}
(8)PA1输出PWM(timer2定时器)
不需要我们去开启、关闭定时器,只需要设置PWM波的占空比。见dataProcess().
(9) sysCount的定时实现
sysCount里有3个时间间隔,LED2的0.1s闪烁、低高频模式切换的5s判断、速度保持2s后的统计,这三个计数值都需要定时器。
这里是使用了滴答定时器systick,1ms触发一次中断。
/**
* @brief This function handles System tick timer.
* @source location stm32g4xx_it.c
*/
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
// time continue 5s
if(LED2Flag && (sysCount[0] < 5000) )
{
if( sysCount[0]%200 == 0) // 步进值200
{
if(modeFreqCount == 0) // 低频模式
PA1freq[modeFreqCount] -= 5;
else
PA1freq[modeFreqCount] += 5;
__HAL_TIM_SetAutoreload(&htim2,PA1freq[modeFreqCount]);
HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE);
}
sysCount[0]++;
}
// time continue 0.1s
if(sysCount[1] < 100)
sysCount[1]++;
// time continue 2s
if(sysCount[2] < 2000)
sysCount[2]++;
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
至于为什么是1ms触发一次中断?
- 首先,在时钟树中配置了systick的频率为80MHz;其次,我们需要知道systick的重装载值,也就是下面的SysTick_Type ->LOAD。在HAL库中,会默认生成1ms触发中断。
追踪HAL库代码。文件位置“stm32g4xx_hal.c”.
/**
* @brief This function configures the source of the time base:
* The time source is configured to have 1ms time base with a dedicated
* Tick interrupt priority.
* @note This function is called automatically at the beginning of program after
* reset by HAL_Init() or at any time when clock is reconfigured by HAL_RCC_ClockConfig().
* @note In the default implementation, SysTick timer is the source of time base.
* It is used to generate interrupts at regular time intervals.
* Care must be taken if HAL_Delay() is called from a peripheral ISR process,
* The SysTick interrupt must have higher priority (numerically lower)
* than the peripheral interrupt. Otherwise the caller ISR process will be blocked.
* The function is declared as __weak to be overwritten in case of other
* implementation in user file.
* @param TickPriority: Tick interrupt priority.
* @retval HAL status
*/
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
HAL_StatusTypeDef status = HAL_OK;
if (uwTickFreq != 0U)
{
/* Configure the SysTick to have interrupt in 1ms time basis*/
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) == 0U)
{
/* Configure the SysTick IRQ priority */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
status = HAL_ERROR;
}
}
else
{
status = HAL_ERROR;
}
}
else
{
status = HAL_ERROR;
}
/* Return function status */
return status;
}
如果想手动调整中断触发间隔,需要修改的是uwTickFreq,stm32g4xx_hal.c中默认设置为1KHz。