STM32G431-Nucleo-64开发板使用小记

STM32G431-Nucleo-64开发板使用小记

STM32G431-Nucleo-64开发板

基本介绍

实物图片:en.nucleo-g431rb

引脚示意图:

image-20220625140101348

特别提醒:如果要设置频率为170MHz(最大频率),需要修改输入为24MHz

image-20220702201055670

点亮LED灯

软件选择

使用CubeMX与MAD-ARM

  • STMCubeMX:

    STMCubeMX是ST公司推出的一种自动创建单片机工程及初始化代码的工具。

  • MDK-ARM:

    Keil公司开发的ARM开发工具MDK,是用来开发基于ARM核的系列微控制器的嵌入式应用程序。

实验流程

使用 CubeMX 生成初始化代码 -> 使用 MDK-ARM 编写主函数并编译 -> 使用开发板自带的ST-LINK将编译好的程序烧录到开发板 -> 搭建实物电路 -> 开发板上电,观察现象。

具体操作

打开CubeMX,可以看到如下界面。点击红色方框内的ACCESS TO MCU SELECTRO选择芯片型号。

image-20220624155754981

输入芯片型号查找对应芯片,选择“STM32G431RBT6”芯片。具体操作如图所示,操作执行后点击Start Progect进入配置页面。

image-20220624161212803

芯片配置页面如下:

image-20220624161504627

配置包括 “Pinout & Configuration”、“Clock Configuration”、“Project Manager” 和 “Tools”。

首先配置 “Project Manager”,方式如下:其中 “Code Generator” 和 “Advanced Setings” 暂时用不到。

image-20220624162039334

想要点亮LED灯,我们需要对GPIO引脚进行控制,因此返回 “Pinout & Configuration” 配置选项卡,进行输入输出引脚配置。我们选择引脚 PC3 ,将其初始化为高电平,并将输出电平再高低之间进行切换,使用HEL函数延时,每隔1000ms进行一次变换。具体操作如图所示:

image-20220624162854160

最后点击生成代码。

image-20220624163308481

使用MDK-ARM打开工程,查看CubeMX生成的代码:

image-20220624164703328

在主函数中插入我们要编写的LED闪烁代码:

image-20220624172922991

代码如下:

1
2
3
4
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_3, GPIO_PIN_SET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_3, GPIO_PIN_RESET);
HAL_Delay(1000);
image-20220624173238440

连接电路图,结果展示:

image-20220624174456175

开发板配置

开发板中的用户按键是高电平有效,默认状态下接PC13。

image-20220702140416463

ST Board 的默认配置中使用外部中断配置按键。

如果使用GPIO口输入模式来实现按键行为,则需要配置为 Pull-down 模式

image-20220624174830465

基本工程配置:

image-20220624175021820

直接生成工程:

image-20220624175042732

可以看到,针对于这个开发板,默认芯片时钟配置如下:

image-20220624180243123

学习使用CubeMX中的Example

首先打开CubeMX进入工程选项界面,选择 “Example Selector” 。

image-20220624192415831

续:

image-20220624192555608

在生成选项中,选择生成目录,然后直接使用MDK-ARM打开即可:

image-20220624192653458

打开文档中的readme.txt文件,阅读该例程的使用方法:

image-20220624193003158

阅读文档可知,PA08-PA11分别输出四种不同占空比的PWM波形。

编译文件并烧录到开发板中,使用示波器测试PWM波形,得如下结果:

如果对Example中的代码进行了修改并重新编译,可能烧录会出现报错,报错如下:

image-20220625170231754

该报错可阅读此文章寻求解决方案。

按键控制LED灯

首先打开之前 Nucleo-G431RB 的 CubeMX 工程文档,打开其中的 “Nucleo-G431RB_TEST.ioc” 文件:

image-20220625162251778

将其中的 PC2 GPIO口配置为输入 IO 口,具体配置为输入模式、上拉输入。

注意:选择 GPIO 口的输入配置要依据实际电路的连接方式。在我的电路连接中,我将按键的一端与开发板的 GND 相连,另一端与 PC2 相连。则此时应该选择上拉输入——IO 口没有外部信号输入时,STM32 检测到是高电平,有信号时(按键按下时),跟随信号电平(接地,变为低电平)。

将 PC3 GPIO口配置为输出,默认输出为高电平(执行初始化函数 MX_GPIO_Init() 时会输出高电平)。

image-20220625162220949

生成代码,使用MDK-ARM打开生成好的工程文件,在工程文件中的 main.c 文件做如下修改:

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
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define BUTTON HAL_GPIO_ReadPin(Button_In_GPIO_Port, Button_In_Pin)
/* USER CODE END PD */

//省略……

int main(void)
{

//省略……

/* Infinite loop */
/* USER CODE BEGIN WHILE */
HAL_GPIO_WritePin(User_White_LED_GPIO_Port, User_White_LED_Pin, GPIO_PIN_RESET);
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(BUTTON == 0)
{
HAL_Delay(10);
if(BUTTON == 0)
{
while(BUTTON == 0);
HAL_GPIO_TogglePin(User_White_LED_GPIO_Port, User_White_LED_Pin);
}
}
}
/* USER CODE END 3 */
}

修改完成后,即可连接电路,检验效果。上述代码执行效果为:

按下按键时可以检测到按键被按下,松手后执行任务(改变LED灯亮灭)。

效果如图:

串口通信

STM32中串口通信有三种方式:

  • 轮询

    • 轮询式串口通信:在主函数的 while(1) 中不停地调用串口通信函数进行通信。
    • 优点:实现逻辑简单
    • 缺点:查询方式下CPU的负担较重,浪费了处理器的能力,不能够很好的处理其他的事件
  • 中断

    • 中断式串口通信:在接收到信息或需要发送数据时产生中断,在中断服务程序中完成数据的接收与发送。

    • 优点:相比于轮询式,中断式对CPU利用率要高。

    • 缺点:

      复杂的系统中,比如移动机器人,处理器需要处理串行口通信,多个传感器数据的采集以及处理,实时轨迹的生成,运动轨迹插补以及位置闭环控制等等任务,牵扯到多个中断的优先级分配问题。为了保证数据发送与接收的可靠性,需要把UART的中断优先级设计较高,但是系统可能还有其他的需要更高优先级的中断,必须保证其定时的准确,这样就有可能造成串行通讯的中断不能及时响应,从而造成数据丢失。

  • DMA

    • DMA:Direct Memory Access (直接内存访问)。使用DMA进行串口通信时,CPU只需要数据传输开始和结束时做一点处理外,在传输过程中可以进行其他的工作。
    • 如果传输的数据量较大,或者传输速度超过115200时,建议选择DMA方式实现串口通信。

串口通信函数:推荐使用C标准库中的 printf() 函数进行串口通信。想要在单片机使用 printf() 需要:

  1. 包含头文件 #include <stdio.h>
  2. 在 Options for Target... 选项卡的 Target 选项栏中的 Code Generation 区域勾选 Use MicroLIB
  3. 进行串口重定向——即重新实现 fputc()

查询模式 & printf()函数重定向

配置串口:注意在Nucleo-G431RB开发板上只能使用 PA2 和 PA3 引脚来实现开发板接usb线与电脑进行串口通信。其中 PA2 和 PA3 引脚可以选择 LPUART1 和 USART2 两种。这里我们选择 LPUART1 进行测试。串口调试的配置与串口测试软件保持一致即可。

image-20220627201123958

生成代码,打开工程,进行工程设置:

image-20220627201706218

如果不设置使用 Use MicroLIB,也可以添加如下代码实现串口重定向:

image-20220703135818223

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
/* USER CODE BEGIN 0 */
#include "stdio.h"

#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};

FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}

#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)//如果使用GCC编译
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)//keil中适用
#endif

PUTCHAR_PROTOTYPE
{
uint8_t temp[1] = {ch};
HAL_UART_Transmit(&hlpuart1, temp, 1, 2);//hlpuart1需要根据你的配置修改
return ch;
}
/* USER CODE END 0 */

添加代码:

  1. 添加头文件:user_log.h文件见目录: “HAL库小记 -> 串口调试策率 -> 日志打印文件”

    image-20220627202133564

    1
    2
    3
    4
    5
    /* Private includes ----------------------------------------------------------*/
    /* USER CODE BEGIN Includes */
    #include <stdio.h>
    #include "user_log.h"
    /* USER CODE END Includes */
  2. 重写 fputc() 函数:

    image-20220627201930878

    1
    2
    3
    4
    5
    6
    int fputc(int ch, FILE *f)
    {
    uint8_t temp[1] = {ch};
    HAL_UART_Transmit(&hlpuart1, temp, 1, 2);//hlpuart1需要根据你的配置修改
    return ch;
    }

    记录另一种写法,或许会更加规范(适用范围广)

    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
    #ifdef __GNUC__
    #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)//如果使用GCC编译
    #else
    #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)//keil中适用
    #endif

    PUTCHAR_PROTOTYPE
    {
    HAL_UART_Transmit(&huart2, (uint8_t*) &ch, 1, 0xffff);
    return ch;
    }


    //网上说现在还需要加上这个函数,但是我没使用过gcc编译所以没有验证过这部分代码

    //_write函數在syscalls.c中, 使用__weak定義, 所以可以直接在其他文件中定義_write函數
    #ifdef __GNUC__
    __attribute__((weak)) int _write(int file, char *ptr, int len)
    {
    int DataIdx;
    for (DataIdx = 0; DataIdx < len; DataIdx++)
    {
    __io_putchar(*ptr++);
    }
    return len;
    }
    #endif

  3. 添加测试代码:

    image-20220627202102623

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
     /* USER CODE BEGIN 2 */
    user_main_info("init HAL");
    user_main_info("Config System Clock");
    user_main_info("GPIO init finished");
    user_main_info("UART init finished");
    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    user_printf("___ ___ _ ");
    user_printf("| \\/ | | | ");
    user_printf("| . . | _ _ | | ___ __ _ ");
    user_printf("| |\\/| || | | || | / _ \\ / _` |");
    user_printf("| | | || |_| || |____| (_) || (_| |");
    user_printf("\\_| |_/ \\__, |\\_____/ \\___/ \\__, |");
    user_printf(" __/ | __/ |");
    user_printf(" |___/ |___/ ");

    user_main_info("Enter while(1)");

测试结果:

image-20220627202356953

中断方式

使用CubeMX配置串口,打开相应串口中断

image-20220703175857659

打开工程,修改 uart.c 实现串口重定向:

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
/* USER CODE BEGIN 0 */
#include "stdio.h"

#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};

FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}

#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)//如果使用GCC编译
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)//keil中适用
#endif

PUTCHAR_PROTOTYPE
{
uint8_t temp[1] = {ch};
HAL_UART_Transmit(&hlpuart1, temp, 1, 2);//hlpuart1需要根据你的配置修改
return ch;
}
/* USER CODE END 0 */

打开 main.c,配置中断服务函数

DMA

DMA传输方式

方法1:DMA_Mode_Normal,正常模式,

当一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次 

方法2:DMA_Mode_Circular ,循环传输模式

当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。也就是多次传输模式

DMA原理

Map回调函数

https://www.its203.com/article/qq_26575553/89374803#:~:text=%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8F1%E7%9A%84MCU%EF%BC%8C%E4%B9%9F%E5%8F%AF%E4%BB%A5%E6%98%AFF2...F4%EF%BC%8C%E7%94%9A%E8%87%B3

1.8寸 TFT LCD显示

使用CubeMX配置引脚,其中 PA5 引脚连接VCC供电。

image-20220627183433259

生成代码,然后对代码进行修改:

  1. 解压LCD_Driver.zip到CubeMX生成的MDK-ARM文件加中:

    image-20220627200134162

  2. 打开工程,将解压的文件添加的你的工程中:

    image-20220627200249439

  3. 将头文件包含到main.c文件中:

    1
    2
    3
    4
    5
    6
    /* Private includes ----------------------------------------------------------*/
    /* USER CODE BEGIN Includes */
    #include "lcd_init.h"
    #include "lcd.h"
    #include "pic.h"
    /* USER CODE END Includes */
  4. 打开lcd_init.h文件,修改其中的 “LCD端口定义” ,将端口定义与连线相匹配。

  5. 注释掉主函数中的 MX_GPIO_Init(); 并将复制下述代码进行测试:

    1
    2
    3
    LCD_Init();//LCD初始化
    LCD_Fill(0,0,LCD_W,LCD_H,WHITE);
    LCD_ShowString(24,30,"Hello,World!",RED,WHITE,16,0);
    image-20220627200731016

定时器

使用STM32的定时器首先要理解STM32的时钟树,STM32中的时钟源是可配置的。有三种时钟源供选择:

image-20220628140231418

由一个三路选择器组成的系统时钟选择器,可以选择下面三种时钟源的一种作为系统时钟源:

  • 高速外部时钟HSE
    • 优点:外部时钟产生的时钟频率较为精确
  • 高速内部时钟HSI
    • 优点:功耗低,不需要额外的器件,起震快
    • 缺点:但是精度不能保证
  • 锁相环时钟PLLCLK
    • 锁相环可以用来倍频,开发板上外接8M晶振,但是STM32主频却能跑72M,这离不开锁相环(PLL)的作用。

定时器要实现计数必须有个时钟源,基本定时器时钟只能来自内部时钟,高级控制定时器和通用定时器还可以选择外部时钟源或者直接来自其他定时器等待模式

F407有三种时钟源可以用作系统时钟:内部高速时钟、外部高速时钟、PLL时钟。 一般我们希望芯片工作在最高频率168MHz,而无论是内部还是外部时钟都是达不到的,所以通常都是用PLL时钟作为系统时钟。 外部时钟通常都比内部时钟要稳定精确,所以一般还会用外部时钟作为PLL的输入。 F407还有一个低速时钟用来驱动RTC,以及满足低电压模式下的功能需求。

通常系统总线AHB的频率设置为168MHz,高速外设总线APB2频率设置为84MHz,低速外设总线APB1频率设置为42MHz。 这些总线频率可以通过配置RCC_CFGR和RCC_PLLCFGR实现。

GPS模块

GPS模块硬件可以将卫星传来的信号进行收集,然后我们可以通过串口通信的方式从GPS模块中读取卫星报文。读取后的报文我们可以将其缓存下来,然后进行译码。

了解了 NMEA 格式有之后,我们就可以编写相应的解码程序了,而程序员 Tim(xtimor@gmail.com)提供了一个非常完善的 NMEA 解码库,在以下网址可以下载到:http://nmea.sourceforge.net/ ,直接使用该解码库,可以避免重复发明轮子的工作。

移植 MultiButton

MultiButton简介

Github里面的嵌入式开源项目,一个小巧简单易用的事件驱动型按键驱动模块,可无限量扩展按键,能够实现下述按键事件:

事件 说明
PRESS_DOWN 按键按下,每次按下都触发
PRESS_UP 按键弹起,每次松开都触发
PRESS_REPEAT 重复按下触发,变量repeat计数连击次数
SINGLE_CLICK 单击按键事件
DOUBLE_CLICK 双击按键事件
LONG_PRESS_START 达到长按时间阈值时触发一次
LONG_PRESS_HOLD 长按期间一直触发

在STM32G431-Nucleo-64开发板中使用

首先使用CubeMX建立一个工程,初始化串口、测试LED和测试按键的GPIO口:

image-20220702160405595

生成代码,打开工程文件夹:

image-20220702160514425

然后在此处下载项目文件到本地:

image-20220702160023733

将红框内的源文件放入到工程文件夹的MDK-ARM文件夹中,这里我给源文件了一个单独的文件夹

image-20220702160647623

同时我也移植了我们之前使用的 “user_log.h” 文件。

打开工程,从工程外部添加文件:

image-20220702161216790

image-20220702161310298

源文件中的头文件,可以添加进来,也可以不添加,只要添加头文件的查找地址,然后在写代码时包含头文件即可:

image-20220702161547708

编写测试代码

  1. 打开 uart.c 文件,添加串口重定向代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /* USER CODE BEGIN 0 */
    #include "stdio.h"
    #ifdef __GNUC__
    #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)//如果使用GCC编译
    #else
    #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)//keil中适用
    #endif

    PUTCHAR_PROTOTYPE
    {
    uint8_t temp[1] = {ch};
    HAL_UART_Transmit(&hlpuart1, temp, 1, 2);//hlpuart1需要根据你的配置修改
    return ch;
    }
    /* USER CODE END 0 */
  2. 打开 main.c 文件

    1. 添加新的包含

      1
      2
      3
      4
      5
      6
      /* Private includes ----------------------------------------------------------*/
      /* USER CODE BEGIN Includes */
      #include "stdio.h"
      #include "user_log.h"
      #include "multi_button.h"
      /* USER CODE END Includes */
    2. 添加新的变量声明——按键结构体声明

      1
      2
      3
      4
      5
      /* Private variables ---------------------------------------------------------*/

      /* USER CODE BEGIN PV */
      struct Button button1;
      /* USER CODE END PV */
    3. 回调函数:

      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
      /* Private user code ---------------------------------------------------------*/
      /* USER CODE BEGIN 0 */
      uint8_t read_button1_GPIO()
      {
      return HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin);
      }
      void button_callback(void *button)
      {
      uint32_t btn_event_val;
      btn_event_val = get_button_event((struct Button *)button);
      switch(btn_event_val)
      {
      case PRESS_DOWN:
      printf("---> key1 press down! <---\r\n");
      break;
      case PRESS_UP:
      printf("***> key1 press up! <***\r\n");
      break;
      case PRESS_REPEAT:
      printf("---> key1 press repeat! <---\r\n");
      break;
      case SINGLE_CLICK:
      printf("---> key1 single click! <---\r\n");
      break;
      case DOUBLE_CLICK:
      printf("***> key1 double click! <***\r\n");
      break;
      case LONG_PRESS_START:
      printf("---> key1 long press start! <---\r\n");
      break;
      case LONG_PRESS_HOLD:
      printf("***> key1 long press hold! <***\r\n");
      break;
      }
      }
    4. 进入 main() 函数,while(1) 之前:初始化对象 -> 注册函数 -> 启动函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_LPUART1_UART_Init();
      /* USER CODE BEGIN 2 */
      HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);
      printf("MultiButton Test...\r\n");

      //初始化按键对象
      button_init(&button1, read_button1_GPIO, 1,0);

      //注册函数
      button_attach(&button1, PRESS_DOWN, button_callback);
      button_attach(&button1, PRESS_UP, button_callback);
      // button_attach(&button1, PRESS_REPEAT, button_callback);
      // button_attach(&button1, SINGLE_CLICK, button_callback);
      // button_attach(&button1, DOUBLE_CLICK, button_callback);
      // button_attach(&button1, LONG_PRESS_START, button_callback);
      // button_attach(&button1, LONG_PRESS_HOLD, button_callback);

      //启动按键
      button_start(&button1);

      /* USER CODE END 2 */
    5. 设置一个5ms间隔的定时器循环调用后台处理函数,可以在while(1)中使用滴答定时器:

      1
      2
      3
      4
      5
      6
      7
      8
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
      //每隔5ms调用一次后台处理函数
      button_ticks();
      HAL_Delay(5);
      /* USER CODE END WHILE */
  3. 勾选使用 MicroLIB

    image-20220702162518916

  4. 编译,烧录,打开串口助手,测试:

    image-20220702162754097

移植 GuiLite

简介

GuiLite是一个轻量的,可以运行在MPU平台的开源图形库。

只要能点亮你的显示屏上一个像素点,就可以使用这个图形库绘制出复杂的图形。

GuiLite还支持多个平台:

  • 支持的操作系统:iOS/macOS/WatchOS,Android,Linux(ARM/x86-64),Windows(包含VR),RTOS... 甚至无操作系统的单片机

  • 支持的开发语言: C/C++, Swift, Java, Javascript, C#, Golang...

  • 支持的第3方库:Qt, MFC, Winforms, CoCoa...

⚙️️最低硬件要求:

Processor Disk/ROM space Memory
24 MHZ 29 KB 9 KB

在STM32G431-Nucleo-64开发板中使用

在TFT屏示例的基础上进行GUI测试:

此处注意,可能会需要扩大默认的堆空间长度,具体操作如下:

  1. CubeMX中修改堆的大小:

    image-20220702213417565

  2. 打开stm32的启动代码(汇编代码)即可看到修改成功:

    image-20220702213543120

移植过程:

  1. 点击此处下载源代码,在此处下载Example;我们需要Example进行测试。

  2. 将Example中的实例代码放到自己的工程中,以Hello3DWave为例:

    image-20220702214620179

    将这两个文件复制到自己的工程中,可以新建一个名为UIcode的文件夹存放。

  3. 在自己的keil工程中的Target中新建一个Group:

    image-20220702214000568

  4. 将例程代码添加到Group中:

    image-20220702214312012

  5. 添加完成后即可编写代码,注意:检查自己的工程中是否勾选了Use MicroLIB,如果勾选了就要取消勾选,因为我们需要编译器去解析.cpp文件,不能使用C库

    image-20220702214859697

    编写测试代码

    1. 在 main.c 中重新实现延时和绘制像素点:

      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
      /* USER CODE BEGIN 0 */
      //延时1ms函数
      void delay_ms(int ms)
      {
      HAL_Delay(ms);
      }
      //RGB888转RGB565
      //Transfer GuiLite 32 bits color to your LCD color
      #define GL_RGB_32_to_16(rgb) (((((unsigned int)(rgb)) & 0xFF) >> 3) | ((((unsigned int)(rgb)) & 0xFC00) >> 5) | ((((unsigned int)(rgb)) & 0xF80000) >> 8))
      //Encapsulate your LCD driver:
      void gfx_draw_pixel(int x, int y, unsigned int rgb)
      {
      //LCD_Fast_DrawPoint(x, y, GL_RGB_32_to_16(rgb));
      //添加带颜色的画点函数
      LCD_DrawPoint(x, y, GL_RGB_32_to_16(rgb));
      }
      //UI entry
      struct DISPLAY_DRIVER
      {
      void (*draw_pixel)(int x, int y, unsigned int rgb);
      void (*fill_rect)(int x0, int y0, int x1, int y1, unsigned int rgb);
      } my_driver;

      //extern function
      /* USER CODE END 0 */
    2. 上面代码中的倒数第二行 //extern function 中的函数声明要去 UIcode.cpp 的最后去找

      image-20220704151857006
    3. 将此函数声明复制到 //extern function 处,然后在 while(1) 前添加如下代码:

      1
      2
      3
      my_driver.draw_pixel = gfx_draw_pixel;
      my_driver.fill_rect = NULL;//gfx_fill_rect;
      //function like: startHelloStar(NULL,128,160,2,&my_driver);
    4. 编译然后烧录,不需要管警告信息。

HAL库小记

硬件抽象层(HAL,Hardware Abstraction Layer)驱动程序提供了一组功能丰富,易于与应用上层交互的 API,它们涵盖了常见的外围设备,可以非常方便的向其它型号 STM32 微控制器移植。同时还实现了用户回调函数机制,允许并发调用 USART1 以及 USART2 等外设,并且支持轮询中断DMA 三种 API 编程模式。

对于HAL库的介绍

GPIO口

初始化及重置函数
1
2
3
4
//初始化引脚 
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
//重置引脚
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin);
GPIO 口操作相关函数
1
2
3
4
5
6
7
8
//读取电平状态 
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
//设置引脚状态
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
//转换引脚状态
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
//锁定引脚状态
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
GPIO口枚举常量
1
2
3
4
5
typedef enum
{
GPIO_PIN_RESET = 0U,
GPIO_PIN_SET
} GPIO_PinState;
GPIO口名称
1
2
3
4
5
6
7
/* IO口默认定义 -----------------------------------------------------------*/
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */

/* Private defines -----------------------------------------------------------*/
#define User_Label_Pin GPIO_PIN_3
#define User_Label_GPIO_Port GPIOA

GPIO的API

函数名称 功能描述
HAL_GPIO_ReadPin() 读取指定输入端口的引脚状态;
HAL_GPIO_WritePin() 设置或者清除指定的数据端口位;
HAL_GPIO_TogglePin() 切换指定的 GPIO 引脚状态;
HAL_GPIO_LockPin() 锁定 GPIO 引脚配置寄存器;
HAL_GPIO_EXTI_IRQHandler() 该函数用于处理 EXTI 中断请求;
HAL_GPIO_EXTI_Callback() EXTI 线检测回调函数;

串口通信

收发函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//发送数据
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size, uint32_t Timeout);
//接收数据
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size, uint32_t Timeout);
//发送中断
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *
pData, uint16_t Size);
//接收中断
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData
, uint16_t Size);
//使用DMA发送
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *
pData, uint16_t Size);
//使用DMA接收
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *
pData, uint16_t Size);
//DMA暂停
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);
//DMA恢复
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);
//DMA停止
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);
printf() 重定向
1
2
3
4
5
6
7
8
#include <stdio.h>

//在 USER CODE BEGIN 0 区域内添加:
int fputc(int ch, FILE *f){
uint8_t temp[1] = {ch};
HAL_UART_Transmit(&huart1, temp, 1, 2);//huart1需要根据你的配置修改
return ch;
}

串口调试策略

日志打印文件

keil编译器要求文件的结尾必须要有一个空行,如果没有将给出一个警告

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
/**
**************************************************************************************
* @file : user_log.h
* @brief : Print your Log information
**************************************************************************************
* @useage
* Include this header file in the program file that you want to print logs.
* Using the function:
* `user_printf()`
* `user_main_info()`
* `user_main_debug()`
* `user_main_error()` to print the log.
* Define "PRINT_USER_LOG" if you want to print logs and annotations it if you don't.
**************************************************************************************
**/
#ifndef __USER_LOG_H
#define __USER_LOG_H

#define PRINT_USER_LOG //define for print log

#ifdef PRINT_USER_LOG

#define user_printf(format, ...) printf( format "\r\n", ##__VA_ARGS__)

#define user_main_info(format, ...) printf(" [info] main.c :" format "\r\n", ##__VA_ARGS__)
#define user_main_debug(format, ...) printf(" [debug] main.c :" format "\r\n", ##__VA_ARGS__)
#define user_main_error(format, ...) printf(" [error] main.c :" format "\r\n",##__VA_ARGS__)

#define user_file_info(format, ...) printf(" [info] file.c :" format "\r\n", ##__VA_ARGS__)
#define user_file_debug(format, ...) printf(" [debug] file.c :" format "\r\n", ##__VA_ARGS__)
#define user_file_error(format, ...) printf(" [error] file.c :" format "\r\n",##__VA_ARGS__)

#else

#define user_printf(format, ...)

#define user_main_info(format, ...)
#define user_main_debug(format, ...)
#define user_main_error(format, ...)

#define user_file_info(format, ...)
#define user_file_debug(format, ...)
#define user_file_error(format, ...)

#endif /* PRINT_USER_LOG */

#endif /* __USER_LOG_H */

添加文件时若出现报错,则需要手动增加头文件搜索路径:

image-20220627212005510
日志打印个性化文字

Text to ASCII Art Generator (TAAG)

1
2
3
4
5
6
7
8
___  ___        _                   
| \/ | | |
| . . | _ _ | | ___ __ _
| |\/| || | | || | / _ \ / _` |
| | | || |_| || |____| (_) || (_| |
\_| |_/ \__, |\_____/ \___/ \__, |
__/ | __/ |
|___/ |___/

代码:要在报错的符号前面加上一个反斜杠,因此代码如下

1
2
3
4
5
6
7
8
user_printf("___  ___        _                   ");
user_printf("| \\/ | | | ");
user_printf("| . . | _ _ | | ___ __ _ ");
user_printf("| |\\/| || | | || | / _ \\ / _` |");
user_printf("| | | || |_| || |____| (_) || (_| |");
user_printf("\\_| |_/ \\__, |\\_____/ \\___/ \\__, |");
user_printf(" __/ | __/ |");
user_printf(" |___/ |___/ ");

分割线内内容摘自Uinlo的个人博客:

HAL 通用命名规则

对于共有的系统外设,无需使用指针或者实例对象,这个规则适用于 GPIOSYSTICKNVICRCCFLASH 外设,例如函数 HAL_GPIO_Init() 只需要 GPIO 的地址及其配置参数。

1
2
3
HAL_StatusTypeDef HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *Init) {
/* GPIO 初始化体 */
}

每个外设驱动程序当中都定义有处理中断和特定时钟配置的,这些宏会被导出到外设驱动的头文件,以便于扩展文件使用,这些用于处理中断和特定时钟配置的宏如下所示:

宏定义 功能描述
__HAL_PPP_ENABLE_IT(__HANDLE__, __INTERRUPT__) 使能一个特定的外设中断;
__HAL_PPP_DISABLE_IT(__HANDLE__, __INTERRUPT__) 失能一个特定的外设中断;
__HAL_PPP_GET_IT (__HANDLE__, __ INTERRUPT __) 获取一个指定外设的中断状态;
__HAL_PPP_CLEAR_IT (__HANDLE__, __ INTERRUPT __) 清除一个指定外设的中断状态;
__HAL_PPP_GET_FLAG (__HANDLE__, __FLAG__) 获取一个指定外设的标志位状态;
__HAL_PPP_CLEAR_FLAG (__HANDLE__, __FLAG__) 清除一个指定外设的标志位状态;
__HAL_PPP_ENABLE(__HANDLE__) 使能一个外设;
__HAL_PPP_DISABLE(__HANDLE__) 失能一个外设;
__HAL_PPP_XXXX (__HANDLE__, __PARAM__) 指定 PPP 外设驱动的
__HAL_PPP_GET_ IT_SOURCE (__HANDLE__, __INTERRUPT__) 检查指定的中断源

注意NVICSYSTICK 是 ARM Cortex-M4 提供的两个核心功能,与之相关的 API 都位于 stm32f4xx_hal_cortex.c 源文件。

当从寄存器读取状态标志位时,其结果由移位值组成,具体取决于读取值的数量与大小。这种情况下,返回的状态宽度为 32 位,例如:

1
2
3
STATUS = XX | (YY << 16)
/* 或者 */
STATUS = XX | (YY << 8) | (YY << 16) | (YY << 24)

外设 PPP 的指针在调用 HAL_PPP_Init() 之前有效,初始化函数会在修改指针字段之前进行检查:

1
2
3
4
HAL_PPP_Init(PPP_HandleTypeDef)
if (hppp == NULL) {
return HAL_ERROR;
}

可以使用条件式宏定义或者伪代码宏定义

  • 条件式宏定义:

    1
    #define ABS(x) (((x) > 0) ? (x) : -(x))
  • 伪代码宏定义(多指令宏):

    1
    2
    3
    4
    5
    #define __HAL_LINKDMA(__HANDLE__, __PPP_DMA_FIELD_, __DMA_HANDLE_) \
    do { \
    (__HANDLE__)->__PPP_DMA_FIELD_ = &(__DMA_HANDLE_); \
    (__DMA_HANDLE_).Parent = (__HANDLE__); \
    } while (0)

中断处理程序与回调函数

除了各种 API 函数之外,HAL 固件库外设驱动程序当中还包含有:

  • 用户回调函数;
  • stm32f4xx_it.c 调用的 HAL_PPP_IRQHandler() 外设中断处理程序;

回调函数被定义为带有 weak 属性的空函数,使用时必须在用户代码当中进行定义,HAL 固件库当中存在三种类型的用户回调函数:

  • 外围系统级初始化与反向初始化回调函数 HAL_PPP_MspInit()HAL_PPP_MspDeInit
  • 外理完成回调函数 HAL_PPP_ProcessCpltCallback
  • 错误的回调函数 HAL_PPP_ErrorCallback
回调函数 示例
HAL_PPP_MspInit() HAL_PPP_MspDeInit() 例如 HAL_USART_MspInit(),由 API 函数 HAL_PPP_Init() 进行调用,用于进行外设的系统级初始化(GPIO、时钟、DMA、中断);
HAL_PPP_ProcessCpltCallback 例如 HAL_USART_TxCpltCallback,当处理执行完成时,由外设或者 DMA 中断处理程序进行调用;
HAL_PPP_ErrorCallback 例如 HAL_USART_ErrorCallback,当发生错误时,由外设或者 DMA 中断处理程序进行调用;

HAL 全局初始化

stm32f4xx_hal.c 提供了一组 API 来初始化 HAL 核心实现:

  • HAL_Init():该函数必须在应用程序启动时调用,用于初始化数据和指令,缓存预获取队列,设置 SysTick 定时器(基于 HSI 时钟)每间隔 1ms 产生一个最低优先级中断,将优先级分组设置为 4 位,调用 HAL_MspInit() 用户回调函数来执行系统级初始化(时钟、GPIO、DMA、中断);
  • HAL_DeInit():重置所有外设,调用用户回调函数 HAL_MspDeInit() 执行系统级反向初始化;
  • HAL_GetTick():获取当前 SysTick 定时器的计数值(在 SysTick 中断内递增),用于外设驱动程序处理超时
  • HAL_Delay():通过 SysTick 定时器实现一个以毫秒为单位的延迟;

IO 操作

带有内部数据处理(发送、接收、读/写)的 HAL 函数,通常具备轮询(Polling)中断(Interrupt)DMA 三种处理方式:


底层探究

新建工程

使用windows系统的PowerShell生成Blank template文件的目录树:

1
2
3
4
5
6
7
8
9
10
11
12
PS C:\Users\qjy\Desktop\Blank template> tree /F
卷 Windows-SSD 的文件夹 PATH 列表
卷序列号为 F01E-6275
C:.
Blank_Template.uvoptx
Blank_Template.uvprojx

├─DebugConfig
Target_1_STM32G431RBTx.dbgconf

├─Listings
└─Objects

添加 “startup_stm32g431xx.s” 和 “system_stm32g4xx.c” 两个文件到 Blank template 文件夹目录下:

新建两个文件,编写系统初始化函数和主函数,如图。然后编译。

image-20220628171400275

编译成功,包含一个警告,内容为:void SystemInit(void) 函数没有函数原型,此处可以忽略该警告。

编译后的工程目录树如下:

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
PS C:\Users\qjy\Desktop\Blank template> tree /F
卷 Windows-SSD 的文件夹 PATH 列表
卷序列号为 F01E-6275
C:.
Blank_Template.uvguix.qjy
Blank_Template.uvoptx
Blank_Template.uvprojx
main.c
startup_stm32g431xx.s
system_stm32g4xx.c

├─DebugConfig
Target_1_STM32G431RBTx.dbgconf

├─Listings
Blank_Template.map

└─Objects
Blank_Template.axf
Blank_Template.build_log.htm
Blank_Template.htm
Blank_Template.lnp
Blank_Template_Target 1.dep
main.d
main.o
startup_stm32g431xx.o
system_stm32g4xx.d
system_stm32g4xx.o

其中keil中的project目录树为:

image-20220628171454899

image-20220628181017627
image-20220628181229406

我们知道系统的时钟是通过系统复位和时钟控制(RCC)寄存器配置的。 在第6.3节中列举了25个RCC寄存器的位定义和偏移地址。参考CubeMX中生成的stm32g431xx.h文件,定义如下的结构体用于访问RCC的每个寄存器:

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
/**
* @brief Reset and Clock Control
*/

typedef struct
{
__IO uint32_t CR; /*!< RCC clock control register, Address offset: 0x00 */
__IO uint32_t ICSCR; /*!< RCC internal clock sources calibration register, Address offset: 0x04 */
__IO uint32_t CFGR; /*!< RCC clock configuration register, Address offset: 0x08 */
__IO uint32_t PLLCFGR; /*!< RCC system PLL configuration register, Address offset: 0x0C */
uint32_t RESERVED0; /*!< Reserved, Address offset: 0x10 */
uint32_t RESERVED1; /*!< Reserved, Address offset: 0x14 */
__IO uint32_t CIER; /*!< RCC clock interrupt enable register, Address offset: 0x18 */
__IO uint32_t CIFR; /*!< RCC clock interrupt flag register, Address offset: 0x1C */
__IO uint32_t CICR; /*!< RCC clock interrupt clear register, Address offset: 0x20 */
uint32_t RESERVED2; /*!< Reserved, Address offset: 0x24 */
__IO uint32_t AHB1RSTR; /*!< RCC AHB1 peripheral reset register, Address offset: 0x28 */
__IO uint32_t AHB2RSTR; /*!< RCC AHB2 peripheral reset register, Address offset: 0x2C */
__IO uint32_t AHB3RSTR; /*!< RCC AHB3 peripheral reset register, Address offset: 0x30 */
uint32_t RESERVED3; /*!< Reserved, Address offset: 0x34 */
__IO uint32_t APB1RSTR1; /*!< RCC APB1 peripheral reset register 1, Address offset: 0x38 */
__IO uint32_t APB1RSTR2; /*!< RCC APB1 peripheral reset register 2, Address offset: 0x3C */
__IO uint32_t APB2RSTR; /*!< RCC APB2 peripheral reset register, Address offset: 0x40 */
uint32_t RESERVED4; /*!< Reserved, Address offset: 0x44 */
__IO uint32_t AHB1ENR; /*!< RCC AHB1 peripheral clocks enable register, Address offset: 0x48 */
__IO uint32_t AHB2ENR; /*!< RCC AHB2 peripheral clocks enable register, Address offset: 0x4C */
__IO uint32_t AHB3ENR; /*!< RCC AHB3 peripheral clocks enable register, Address offset: 0x50 */
uint32_t RESERVED5; /*!< Reserved, Address offset: 0x54 */
__IO uint32_t APB1ENR1; /*!< RCC APB1 peripheral clocks enable register 1, Address offset: 0x58 */
__IO uint32_t APB1ENR2; /*!< RCC APB1 peripheral clocks enable register 2, Address offset: 0x5C */
__IO uint32_t APB2ENR; /*!< RCC APB2 peripheral clocks enable register, Address offset: 0x60 */
uint32_t RESERVED6; /*!< Reserved, Address offset: 0x64 */
__IO uint32_t AHB1SMENR; /*!< RCC AHB1 peripheral clocks enable in sleep and stop modes register, Address offset: 0x68 */
__IO uint32_t AHB2SMENR; /*!< RCC AHB2 peripheral clocks enable in sleep and stop modes register, Address offset: 0x6C */
__IO uint32_t AHB3SMENR; /*!< RCC AHB3 peripheral clocks enable in sleep and stop modes register, Address offset: 0x70 */
uint32_t RESERVED7; /*!< Reserved, Address offset: 0x74 */
__IO uint32_t APB1SMENR1; /*!< RCC APB1 peripheral clocks enable in sleep mode and stop modes register 1, Address offset: 0x78 */
__IO uint32_t APB1SMENR2; /*!< RCC APB1 peripheral clocks enable in sleep mode and stop modes register 2, Address offset: 0x7C */
__IO uint32_t APB2SMENR; /*!< RCC APB2 peripheral clocks enable in sleep mode and stop modes register, Address offset: 0x80 */
uint32_t RESERVED8; /*!< Reserved, Address offset: 0x84 */
__IO uint32_t CCIPR; /*!< RCC peripherals independent clock configuration register, Address offset: 0x88 */
uint32_t RESERVED9; /*!< Reserved, Address offset: 0x8C */
__IO uint32_t BDCR; /*!< RCC backup domain control register, Address offset: 0x90 */
__IO uint32_t CSR; /*!< RCC clock control & status register, Address offset: 0x94 */
__IO uint32_t CRRCR; /*!< RCC clock recovery RC register, Address offset: 0x98 */
__IO uint32_t CCIPR2; /*!< RCC peripherals independent clock configuration register 2, Address offset: 0x9C */
} RCC_TypeDef;

RCC基地址宏定义

1
2
3
4
5
#define PERIPH_BASE           (0x40000000UL) /*!< Peripheral base address */
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000UL)
#define RCC_BASE (AHB1PERIPH_BASE + 0x1000UL)

#define RCC ((rcc_regs_t *)RCC_BASE)

那么我们就可以通过如下的形式来访问RCC的寄存器了。

1
2
RCC->CR
RCC->CFGR

CubeMX小记

SYS配置介绍

引用:调试接口配置。讲道理SWD应该是首选,如图5所示。如果不设置的话,编译下载后,你就会发现下载不了程序了,有复位键还好,没复位键就有得愁了。

选择Serial Wire是与下图中的SW相匹配

image-20220630224454477

引用:如果在STM32CubeMX中选择SW协议,MDK 也必须 选择SW协议。JTAG协议配置也同理。否则会造成下载和调试失败。在实际项目中SW协议使用使用的比较多,SW与JTAG相比,速度更快,占用的引脚更少,推荐大家配置成SW协议。

Keil配置

Edit—>Configuration 进行配置

Configuration

  • Editor
    • General—>Encoding—>Chinese GB2312(Simplifies)
    • Funcition—>default
    • Look & Feel—>default
    • Feil & Project Handling—>choose all expect first one
    • C/C++ Files—>Change Tab Size to 4
    • ASM Files—>default
    • Other Files—>default
  • Colors & Fonts
    • C/C++ Editor files—>Test—>Font: Cascadis Code/Size: 20
  • Shortcut Keys
    • Project: Rebulid all target files—>Ctrl+R
    • View: Bulid Output Windows—>Ctrl+B
    • View: Project Windows—>Ctrl+D