开发板

国信长天 CT117E-M4 开发板,stm32G431RBT6 芯片

image-20240408214151812

外部晶振 24MHz

image-20240409191607257

CubeMX配置工程

选芯片 STM32G431RBT6 LQFP封装

image-20240409191934601

高速时钟选择外部晶振

image-20240409192159075

配置时钟树(系统时钟随心配吧,不一定要非要80M)

image-20240409192459350

串口Debug

image-20240409192704326

写项目名,改工具链

image-20240409192839544

选择固件版本及路径(自己看着改改)

image-20240409193512871

固件路径改到对应目录就行(下图是某届蓝桥杯给的固件)

image-20240409193602872

勾选生成外设初始化文件

image-20240409193025799

最后Generate Code即可

Keil5配置

点击小魔术棒,选择调试器为CMSIS-DAP Debugger ,再点setting,在 Flash Download 中选择Reset and Run

image-20240409194639869

在项目目录里新建个文件夹,随便叫啥,比如bsp (板载支持外设)

image-20240409195237374

在项目中新建一个组

image-20240409195437983

也改个名

image-20240409195423432

每次新建文件可以放到dsp里,然后向组里添加现有文件,只添加.c即可

image-20240409195632527

外设

LED 和 锁存器

image-20240408235733947

LED管脚和LCD屏幕有存在GPIO复用,SN74HC573ADWR锁存器 可以隔离信号,防止两个外设干扰。LE管脚(PD2)拉高后锁存器导通,拉低后不导通。

LCD 通过锁存器和 PC8~15 引脚相连,低电平LED导通发光,因此上电初始化时,应默认输出高电平,在CubeMx终设置如下。

image-20240409001317442

keil 中,点亮LED的代码如下:

1
2
3
4
5
6
void LED_Disp(uint16_t dsLED){
HAL_GPIO_WritePin(GPIOC, 0xFF00, 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); //关闭锁存器
}

LCD液晶屏

image-20240409001609892

官方提供 LCD 库,只需要无脑使能引脚output即可

有两个.h 文件和一个.c文件,提供如下 User 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void LCD_SetTextColor(vu16 Color); //文字颜色
void LCD_SetBackColor(vu16 Color);
void LCD_ClearLine(u8 Line);
void LCD_Clear(u16 Color);
void LCD_SetCursor(u8 Xpos, u16 Ypos);
void LCD_DrawChar(u8 Xpos, u16 Ypos, uc16 *c);
void LCD_DisplayChar(u8 Line, u16 Column, u8 Ascii);
void LCD_DisplayStringLine(u8 Line, u8 *ptr);
void LCD_SetDisplayWindow(u8 Xpos, u16 Ypos, u8 Height, u16 Width);
void LCD_WindowModeDisable(void);
void LCD_DrawLine(u8 Xpos, u16 Ypos, u16 Length, u8 Direction);
void LCD_DrawRect(u8 Xpos, u16 Ypos, u8 Height, u16 Width);
void LCD_DrawCircle(u8 Xpos, u16 Ypos, u16 Radius);
void LCD_DrawMonoPict(uc32 *Pict);
void LCD_WriteBMP(u32 BmpAddress);
void LCD_DrawBMP(u32 BmpAddress);
void LCD_DrawPicture(const u8 *picture);

按键

image-20240409002923102

按键按下时为低电平

启用定时器按键消抖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef enum{
Key_Up=0,
Key_Downing,
Key_Down,
Key_Uping,
Key_Default,
}BTN_State; //按键电平状态

typedef enum{
BTN_release=0,
BTN_shortPress,
BTN_longPress,
}BTN_Signal; // 按键信号 松开/短按/长按

extern BTN_Signal btn_signal[4];

两个定时器(10ms 消抖, 100ms判断短按、长按)实现长按、短按功能

设置好 PSCARR

image-20240409003550172

开启中断

image-20240409003653057

代码里记得在tim.c里开启中断:

image-20240409004832369

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim){ //定时器回调函数
static uint8_t led_num = 0;
static BTN_State btn_state[4] = {Key_Up, Key_Up, Key_Up, Key_Up};
static GPIO_TypeDef* BTN_GPIO_Port[4] = {BTN1_GPIO_Port, BTN2_GPIO_Port, BTN3_GPIO_Port, BTN4_GPIO_Port};
static uint16_t BTN_Pin[4] = {BTN1_Pin, BTN2_Pin, BTN3_Pin, BTN4_Pin};
if( htim->Instance == TIM6 ){ // 10ms
for(uint8_t btn = 0; btn < 4; btn++){ // 更新按键状态
if( HAL_GPIO_ReadPin(BTN_GPIO_Port[btn], BTN_Pin[btn]) == GPIO_PIN_RESET ){
if( btn_state[btn] == Key_Up ){
btn_state[btn] = Key_Downing;
}else if( btn_state[btn] == Key_Downing ){
btn_state[btn] = Key_Down;
}else if( btn_state[btn] == Key_Uping ){
btn_state[btn] = Key_Down;
}
}
if( HAL_GPIO_ReadPin(BTN_GPIO_Port[btn], BTN_Pin[btn]) == GPIO_PIN_SET ){
if( btn_state[btn] == Key_Down ){
btn_state[btn] = Key_Uping;
}else if( btn_state[btn] == Key_Uping ){
btn_state[btn] = Key_Up;
}else if( btn_state[btn] == Key_Downing ){
btn_state[btn] = Key_Up;
}
}
}
}else if( htim->Instance == TIM7 ){ //100ms
static BTN_State btn_last_state[4] = {Key_Default, Key_Default, Key_Default, Key_Default};
static uint8_t BTN_Pressed_Time[4] = {0};
static bool level_down[4] = {false, false, false, false};
for(uint8_t i = 0; i < 4; i++ ){
if( btn_state[i] == Key_Down && btn_state[i] != btn_last_state[i] ){ //被按下
BTN_Pressed_Time[i] = 0;
level_down[i] = true;
}
if( level_down[i] ){ //记录时长
BTN_Pressed_Time[i]++;
}
if( BTN_Pressed_Time[i] > 5 ){ // 500ms 超过500ms还未松开
BTN_Pressed_Time[i] = 0;
level_down[i] = false;
/*long press do something */
btn_signal[i] = BTN_longPress; //发出长按信号
/*long press do something */
}else{
if( level_down[i] && btn_state[i] == Key_Up && btn_state[i] != btn_last_state[i] ){ // 松开时不足500ms
level_down[i] = false;
BTN_Pressed_Time[i] = 0;
/*short press do something */
btn_signal[i] = BTN_shortPress; //发出短按信号
/*short press do something */
}
}
btn_last_state[i] = btn_state[i]; //更新传递
}
}
}

在主循环中处理按键信号,使用完记得清空!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void BTN_press_func(){
for(uint8_t i = 0; i < 4; i++){
if( btn_signal[i] == BTN_shortPress ){ //长按
switch (i){
case 0: // BTN1
break;
case 1: // BTN2
break;
case 2: // BTN3
break;
case 3: // BTN4
break;
default: break;
}
btn_signal[i] = BTN_release; //重置信号
}else if (btn_signal[i] == BTN_longPress){ //短按
switch (i){
case 0: // BTN1
break;
case 1: // BTN2
break;
case 2: // BTN3
break;
case 3: // BTN4
default: break;
}
btn_signal[i] = BTN_release; //重置信号
}
}
}

PWM输出

使用定时器的通道输出PWM波,调节CCR寄存器改变占空比。

选定时器和通道,不能选 TIMx_CHxN,因为这是输出互补PWM波

image-20240409004140714

使能定时器,选择PWM生成

image-20240409004418854

设置 PSC 和 ARR ,为了方便调整占空比,ARR尽量设置为 100-1,1000-1,…

image-20240409004515621

代码里记得开启PWM生成

image-20240409004940264

读取和修改占空比

1
2
__HAL_TIM_GetCompare(&htim16, TIM_CHANNEL_1);
__HAL_TIM_SetCompare(&htim16, TIM_CHANNEL_1, val);

定时器输入捕获

用来读取信号发生器产生信号的频率和占空比

image-20240409005902889

信号发生器可以产生频率可调,占空比恒定的信号。通过跳线连接到 PA15 (TIM2_CH1)和 PB4 (TIM3_CH1)引脚。

配置定时器,开启内部时钟源,通道 1 输入捕获直接模式,通道 2 输入捕获间接模式(如果不测占空比的话可以不开CH2)

image-20240409010402375

调节PSC,不调节ARR(默认最大)

image-20240409010647426

开启中断

image-20240409010708737

通道 1 检测上升沿,通道 2 检测下降沿,输入滤波器适当调整(反正0、8都能跑)

image-20240409010731178

代码里开启两个通道的输入捕获中断

image-20240409010922705

重写void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)函数

1
2
3
4
5
6
7
8
9
10
11
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){
if( htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1){ //channel1上升沿 为每个周期的开始
ccr1_val1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1 );
ccr2_val1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2 );
__HAL_TIM_SetCounter(htim, 0); //重置计数器
frq1 = (80000000 / 80) / ccr1_val1; // frq = sys_clk / psc / 一周期定时器计数值
duty1 = (double)ccr2_val1*100 / ccr1_val1; // duty = 高电平持续 / frq * 100%
HAL_TIM_IC_Start(htim, TIM_CHANNEL_1); //重启输入捕获
HAL_TIM_IC_Start(htim, TIM_CHANNEL_2);
}
}

ADC

采集模拟电压

image-20240409011629216

引脚 PB15 和 PB12 分别对应 ADC1_IN11ADC2_IN15

设置单端采样

image-20240409011925620

开启数据复写(可选)

image-20240409012023054

读取ADC并转换为电压值

1
2
3
4
5
double getVoltage(ADC_HandleTypeDef* adc_handle){
HAL_ADC_Start(adc_handle);
uint16_t adc_value = HAL_ADC_GetValue(adc_handle);
HAL_ADC_Stop(adc_handle);
return adc_value * 3.3 / (1<<12);

IIC-eeprom

image-20240409012245407

官方提供 IIC 的库,使用时只需编写简单的读写函数

同样无脑使能 PB6、PB7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
uint8_t eeprom_read(uint8_t addr){ //参数为在 eeprom 中存储的位置
// address: 0x1010000x : x=1 -> read EEPROM x=0 -> write EEPROM
uint8_t msg;
I2CStart();
I2CSendByte(0xA0); //iic 写
I2CWaitAck(); // 等回信
I2CSendByte(addr); //写入读取位置
I2CWaitAck();
I2CStop();

I2CStart(); // 重启
I2CSendByte(0xA1); //iic 读
I2CWaitAck();
msg = I2CReceiveByte(); //iic 接收数据
I2CSendAck();
I2CStop();
return msg;
}

void eeprom_write(uint8_t addr, uint8_t data){ // 一个位置只能存 1 字节
// address: 0x1010000x : x=1 -> read EEPROM x=0 -> write EEPROM
I2CStart();
I2CSendByte(0xA0); //iic 写
I2CWaitAck();
I2CSendByte(addr); //写入存储位置
I2CWaitAck();
I2CSendByte(data); //写入存储数据
I2CWaitAck();
I2CStop();
}

IIC-数字电位计

image-20240409020009717

可以调节 2^7 = 128 ( 0 ~ 127 )个挡位,输入数据一共 7 比特有效位数,输入数据越大,电阻越大最大100k

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint8_t MCP4017_read(void){
// address: 0x0101111x : x=1 -> read MCP4017 x=0 -> write MCP4017
uint8_t val;
I2CStart();
I2CSendByte(0x5f);
I2CWaitAck();
val=I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return val;
}

void MCP4017_write(uint8_t val){
// address: 0x0101111x : x=1 -> read MCP4017 x=0 -> write MCP4017
I2CStart();
I2CSendByte(0x5e);
I2CWaitAck();
I2CSendByte(val);
I2CWaitAck();
I2CStop();
}

可通过 PB14(ADC1_IN5)读取其模拟电压值

image-20240409014006375

ADC1此前已用过 IN11 ,因此使用最简单也最方便的多路ADC采样方法。

禁用连续转换、非连续转换、DMA转换模式,转换次数为1

image-20240409014032403

每次读取时,重新设置ADC采样通道,一定要有HAL_ADC_PollForConversion(adc_handle, HAL_MAX_DELAY);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double getVoltage(ADC_HandleTypeDef* adc_handle, uint32_t Channel){
if( adc_handle->Instance == ADC1 ){
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = Channel;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5;
if (HAL_ADC_ConfigChannel(adc_handle, &sConfig) != HAL_OK) //重置
{
Error_Handler();
}
}
HAL_ADC_Start(adc_handle);
if( adc_handle->Instance == ADC1 ){
HAL_ADC_PollForConversion(adc_handle, HAL_MAX_DELAY); //等待转换 !!!
}
uint16_t adc_value = HAL_ADC_GetValue(adc_handle);
HAL_ADC_Stop(adc_handle);
return adc_value * 3.3 / (1<<12);
}

UART串口

使能串口 USART1

image-20240409014522377

异步模式

image-20240409014553683

使能中断

image-20240409014703467

波特率

image-20240409014616227

串口阻塞发送

1
HAL_UART_Transmit(&huart1, (u8*)rxmsg, strlen(rxmsg), 50);

串口非阻塞接收,重写函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char rxmsg[30];
uint8_t rxloc=0;
uint8_t rxdata;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart){
rxmsg[rxloc++] = rxdata;
rxmsg[rxloc] = 0; //不接收字符串可以不这么干
HAL_UART_Receive_IT(huart, &rxdata, 1); //重启中断,每次中断只能接收 1 个字节
}
// 在主循环中处理接收到的数据
void uart_rx_process(){ // 处理一次接收的数据
if( rxloc != 0 ){
uint8_t temp = rxloc;
HAL_Delay(1); //等一下看看这次接收是不是还没收完
if( temp == rxloc ){
//receive something
...
rxloc = 0; //记得reset
}
}
}