stm32常用模块的函数整理

本文最后更新于:2025年1月21日 凌晨

存放一些常用模块的函数实现,.h.c文件就不放啦~

Stm32CubeIDE的一些使用技巧

  • 调整代码字体大小
    • Ctrl+Shift+'+' 放大,Ctrl +'-' 缩小。放大为啥多了个Shift,因为不按Shift,是等号
  • 导入已有的工程:参考这篇博客
    • 要打开芯片设置页面,直接open工程的.ioc文件。
  • STM32IDE窗口恢复
  • 数组统一赋值/清零操作,不能直接赋值,需要用memset函数,记得使用前检查是否include <string.h>
    1
    memset(rx_data, 0, sizeof(rx_data));//清理缓冲区

问题篇

Stm32CubeIDE相关的问题

1.Confirm Perspective Switch(确认视角切换)

  • 问题描述:
    1
    This kind of launch is configured to open the Debug perspective when it suspends.

弹窗

  • 如果不想每次都这么提示,可以点Remember my decision。点击switch就是切换。

2.Command aborted(命令失败)

  • 问题描述:
    1
    Failed to insert all hardware breakpoints

弹窗

  • 一般解决方式:
    • 减少断点/监视点的数量:硬件调试器通常有硬件断点/监视点的数量限制。你可以尝试删除一些不必要的断点/监视点。
    • 使用软件断点:有些IDE支持软件断点,虽然性能稍逊于硬件断点,但可以避免硬件断点数量限制的问题。
    • 检查断点的有效性:确保所有设置的断点都是在有效的代码位置,而不是在无效或者不适合设置断点的位置。

3.Porblem occurred(发生问题)

  • 问题描述:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    'Launching xxxx' has encountered a problem.

    Error in final launch sequence:

    Failed to execute MI command:
    load xxxxxx

    Error message from debugger back end:
    Error finishing flash operation
    这个问题比较复杂,也遇到很多次,大部分情况,和硬件连接相关,简单说就是硬件没连接好。
  • 解决方式
    解决方式不是唯一吧,这里罗列些。
    • 检查硬件连接:确保调试器和目标设备连接正常。如果有物理连接问题,可能导致这种错误。
    • 重新启动调试器和设备:有时重启调试器和设备可以解决问题。
    • 检查文件路径和权限:确保文件或者路径有访问权限,有正确且有访问权限。
    • 重新编译项目:重新编译项目,以确保生成的ELF文件没有问题。
    • 更新调试器固件:确保使用的是最新版本的调试器固件。

4.使用STM32CubeIDE ST-Link下载提示“Target no device found”

复位管脚复位时,利用下载软件(比如STM32 ST-LINK Utility),连接,清除单片机里的程序,然后再试一次烧录。

调试篇

debug模式,打断点+串口输出调试

参考这篇博客

printf函数输出到串口

keil5-IDE

Keil MDK使用的是ARM编译器,参考这篇博客即可,三种方法。

  • 使用微库(Use MicroLIB)

  • 在 usrat.c 文件中添加如下代码:

    1
    #include <stdio.h>//放在usrat.h开头

    这段代码放在usrat.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
    #pragma import(__use_no_semihosting)             
    //标准库需要的支持函数
    struct __FILE
    {
    int handle;

    };

    FILE __stdout;
    //定义_sys_exit()以避免使用半主机模式
    void _sys_exit(int x)
    {
    x = x;
    }
    //重定义fputc函数
    int fputc(int ch, FILE *f)
    {
    //二选一,功能一样
    HAL_UART_Transmit (&huart1 ,(uint8_t *)&ch,1,HAL_MAX_DELAY );
    return ch;

    // while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
    // USART1->DR = (uint8_t) ch;
    // return ch;
    }
    • 在任意需要使用printf函数打印的C文件中,都需要引用#include <stdio.h>头文件
  • 适合多个串口打印的方法(GCC编译器也可以用)

    • 首先在usrat.c 文件中添加如下代码
      1
      2
      3
      #include <stdarg.h>
      #include <string.h>
      #include <stdio.h>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      void UsartPrintf(UART_HandleTypeDef USARTx, char *fmt,...)
      {

      unsigned char UsartPrintfBuf[296];
      va_list ap;

      va_start(ap, fmt);
      vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap); // 格式化字符串
      va_end(ap);

      // 发送整个字符串
      HAL_UART_Transmit(USARTx, UsartPrintfBuf, strlen((char *)UsartPrintfBuf), HAL_MAX_DELAY);
      }
    • 然后在usrat.h文件中添加如下代码:
      1
      2
      3
      #define USART_DEBUG		huart1 //看硬件选择的是uart几,就在这里写几

      void UsartPrintf(UART_HandleTypeDef USARTx, char *fmt,...);
    • 使用方法:
      1
      UsartPrintf(USART_DEBUG, "The USART1 is OK!\r\n");

    注意:函数参数中 USART_DEBUG 参数为在 usrat.h 中重定义的 huart1 。

    • 如果同时打开了USART1和USART2,那么在 usrat.h 中还会有一个 huart2 ,像huart1(重新define为USART_DEBUG) 一样重定义 huart2 ,和其它串口区分。

Stm32CubeIDE

  • 法一:使用于单个串口调试输出,但是该串口同时使用DMA串口通信时会失效

在usart.h中加入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <errno.h>
#include <sys/unistd.h> // STDOUT_FILENO, STDERR_FILENO

int _write(int file, char *data, int len)
{
if ((file != STDOUT_FILENO) && (file != STDERR_FILENO))
{
errno = EBADF;
return -1;
}

// 实现 USART 发送数据的逻辑
if (HAL_UART_Transmit(&huart1, (uint8_t*)data, len, HAL_MAX_DELAY) != HAL_OK)
{
errno = EIO;
return -1;
}

return len;
}

然后直接使用printf打印即可输出到串口。

  • 法二:适合多个串口打印的方法(同上)

  • 法三:在Private includes 中引入:#include <stdio.h>

    • 再在USERCODEBEGIN0添加:
      1
      2
      3
      4
      5
      int fputc(int ch, FILE *f){
      uint8_t temp[1] = {ch};
      HAL_UART_Transmit(&huart1, temp, 1, 2);//huart1需要根据你的配置修改
      return ch;
      }
      然后你就可以在任意地方使用printf语句方便的输出你想要的内容。

串口数据缓冲区每次用完,记得清理

1
2
3
//重新启动接收,使用Ex函数,接收不定长数据
memset(rx_data, 0, sizeof(rx_data));//清理缓冲区
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_data, sizeof(rx_data));

功能模块篇

矩阵键盘

这里是4行3列的键盘

key.h文件

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
#include "stm32f1xx_hal.h"
#include "gpio.h"
#ifndef _KEY_BOARD_H_
#define _KEY_BOARD_H_


#define KEYBOARD_GPIO_PORT GPIOD
#define KEYBOARD_GPIO_CLK_FUN RCC_APB2PeriphClockCmd
#define KEYBOARD_GPIO_CLK RCC_APB2Periph_GPIOD


//line 行
#define KEYBOARD_GPIO_PIN0 KEY_col0_Pin
#define KEYBOARD_GPIO_PIN1 KEY_col1_Pin
#define KEYBOARD_GPIO_PIN2 KEY_col2_Pin
#define KEYBOARD_GPIO_PIN3 KEY_col3_Pin



//row 列
#define KEYBOARD_GPIO_PIN4 KEY_row0_Pin
#define KEYBOARD_GPIO_PIN5 KEY_row1_Pin
#define KEYBOARD_GPIO_PIN6 KEY_row2_Pin


//extern uint8_t Send_F;

extern void Keyboard_GPIO_Config(void);
extern uint16_t keyboard_scan(void);
#endif

key.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
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
58
59
60
61
62
63
64
65
66
#include "stm32f1xx_hal.h"
#include "key.h"


// 初始化键盘 GPIO
void Keyboard_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};

// 使能 GPIOD 的时钟
__HAL_RCC_GPIOD_CLK_ENABLE();

// 配置列线为输出
GPIO_InitStruct.Pin = KEYBOARD_GPIO_PIN0 | KEYBOARD_GPIO_PIN1 | KEYBOARD_GPIO_PIN2;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(KEYBOARD_GPIO_PORT, &GPIO_InitStruct);

// 配置行线为输入,带上拉电阻
GPIO_InitStruct.Pin = KEYBOARD_GPIO_PIN4 | KEYBOARD_GPIO_PIN5 | KEYBOARD_GPIO_PIN6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(KEYBOARD_GPIO_PORT, &GPIO_InitStruct);
}


uint16_t keyboard_scan(void) {
uint16_t key_val = 0;

// 列和行引脚数组定义
const uint16_t cols[3] = {KEYBOARD_GPIO_PIN0, KEYBOARD_GPIO_PIN1, KEYBOARD_GPIO_PIN2}; // 列引脚
const uint16_t rows[4] = {KEYBOARD_GPIO_PIN4, KEYBOARD_GPIO_PIN5, KEYBOARD_GPIO_PIN6, KEYBOARD_GPIO_PIN3}; // 行引脚

for (int col = 1; col < 3; col++) {
printf("Setting column %d to LOW\n", col);
// 设置当前列为低电平
KEYBOARD_GPIO_PORT->BSRR = (1 << (16 + cols[col])); // Reset part of BSRR sets pin low

for (int row = 1; row < 4; row++) {
printf("Reading row %d state\n", row);
// 检查行状态
if (!(KEYBOARD_GPIO_PORT->IDR & (1 << rows[row]))) { // 检测行是否为低
printf("Key pressed at row %d, col %d\n", row, col);
key_val = 1 + row * 3 + col; // 计算按键值
HAL_Delay(10); // 消抖
printf("Waiting for key release at row %d, col %d\n", row, col);
int timeout = 10000; // 设置一个超时计数器
while (!(KEYBOARD_GPIO_PORT->IDR & (1 << rows[row])) && --timeout) {
//printf("Waiting for key release at row %d, col %d\n", row, col);
}
if (!timeout) {
printf("Timeout waiting for key release.\n"); // 如果超时则打印消息
}
printf("Key released at row %d, col %d\n", row, col);
KEYBOARD_GPIO_PORT->BSRR = (1 << cols[col]); // 将当前列设置回高电平
return key_val; // 返回按键值
}
}

// 将当前列设置回高电平
KEYBOARD_GPIO_PORT->BSRR = (1 << cols[col]);
printf("Setting column %d back to HIGH\n", col);
}

printf("No key pressed\n");
return 0; // 无按键被按下
}

主程序调用

1
2
3
4
5
uint16_t key = keyboard_scan();
if (key != 0) {
printf("Pressed Key: %d\n", key);
}
HAL_Delay(100); // 避免过快重复扫描

串口通信

工程配置

  • 开启外部晶振:在Pinout&Configuration -> System Core -> RCC 页面,将 High Speed Clock (HSE) 配置为 Crystal/Ceramic Resonator
  • 配置时钟频率:在Clock Configuration 页面,将PLL Source 选择为 HSE,将System Clock Mux 选择为 PLLCLK,然后在HCLK (MHz) 输入72并回车,将HCLK频率配置为 72 MHz
  • 打开串口外设:Pinout&Configuration -> Connectivity -> USART1/2/3,将Mode选择为Asynchronous(自行选择串口)
    • Usart3一般是蓝牙串口
  • 添加DMA通道:在 USARTx -> Configuration -> DMA Settings 标签卡中,点击 Add 按钮,分别添加 USARTx_RX 和 USARTx_TX 的 DMA 通道
  • 使能串口中断:在 USART2 -> Configuration -> NVIC Settings 标签卡中,勾选 USARTx global interrupt 的 Enable

收发主体代码

  • 定义全局变量 rx_data 作为串口接收缓冲区,tx_data 作为串口发送缓冲区。
    • 由于是不定长数据的接收,因此缓冲区大小可以根据实际需求调整,只能大不能小,否则可能会丢失数据
      1
      2
      //串口接收缓冲区
      uint8_t rx_data[256] = {0};
  • 在 main 函数中,使用 HAL_UARTEx_ReceiveToIdle_DMA 函数开启不定长数据DMA接收
    • 注意:需要关闭DMA传输过半中断,我们只需要接收完成中断。
    • 此函数是以空闲中断作为接收完成的标志,而不是接收长度,因此可以接收任意长度的数据。
      1
      2
      3
      4
      // 使用Ex函数,接收不定长数据
      HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_data, sizeof(rx_data));
      // 关闭DMA传输过半中断(HAL库默认开启,但我们只需要接收完成中断)
      __HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
  • 在中断函数 HAL_UARTEx_RxEventCallback 中,处理接收到的数据
    • 记得在回调函数结尾,重新启动接收,使用Ex函数,接收不定长数据
    • 记得每次接收完和发送完数据之后,都要清理缓冲区
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 不定长数据接收完成回调函数,这里将接收到的数据又发送出去
      void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
      if (huart == &huart1) {
      data_ready = 1; // 标记数据已准备好
      //在主函数中,检查data_ready,当数据已准备好,进入数据校验和解析过程。
      //重新启动接收,使用Ex函数,接收不定长数据
      memset(data_send, 0, sizeof(data_send));//清理send缓冲区
      HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_data, sizeof(rx_data));
      }
      }

校验部分

奇偶校验

奇偶校验的计算方法(代码思路)

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 processData(uint8_t* data, int size) {
// 解析和处理数据
char message[] = "Start Check!";
HAL_UART_Transmit(&huart1, message, strlen(message), HAL_MAX_DELAY);

uint8_t data_check[256] = {0};
// 处理新消息前重置标志
Red_Flag = Green_Flag = Yello_Flag = 0;

// 将接收到的数据复制到数据检查数组
memcpy(data_check, data, size);

// 清理rx_data缓冲区
memset(data, 0, size);

// 校验数据是否有效,例程的校验思路为头校验,长度校验和奇偶校验,size是有效数据的长度。
if (size > 1 && data_check[0] == 0xAA) {
// 校验数据长度是否匹配
if (data_check[1] == size) {
// 校验奇偶校验位
if (CheckParity(data_check, size)) {
// 校验成功,解析数据包
ParseDataPacket(data_check, size);
char message1[] = "Data processed successfully.";
HAL_UART_Transmit(&huart1, message1, strlen(message1), HAL_MAX_DELAY);
} else {
char message2[] = "Parity check failed.";
HAL_UART_Transmit(&huart1, message2, strlen(message2), HAL_MAX_DELAY);
}
} else {
char message2[] = "Invalid size.";
HAL_UART_Transmit(&huart1, message2, strlen(message2), HAL_MAX_DELAY);
}
} else {
char message2[] = "Invalid frame header.";
HAL_UART_Transmit(&huart1, message2, strlen(message2), HAL_MAX_DELAY);
}
}

int CheckParity(uint8_t* data, uint16_t size) {
uint8_t parity = 0;
for (int i = 0; i < size - 2; i++) {
parity ^= data[i]; // XOR所有数据字节(除了校验位)
}

if (parity == data[size - 1]) {
// 校验成功
char message[] = "Parity check successfully.";
HAL_UART_Transmit(&huart1, message, strlen(message), HAL_MAX_DELAY);
return 1;
} else {
// 校验失败
char message[] = "Parity check failed.";
HAL_UART_Transmit(&huart1, message, strlen(message), HAL_MAX_DELAY);
return 0;
}
}

和校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int checkChecksum(uint8_t* data, uint16_t size) {
uint8_t checksum = 0;

// 对数据包的所有字节(除了最后一个字节,此字节为目标校验和)进行求和
for (int i = 0; i < size - 2; i++) {
checksum += data[i]; // 求和
}

// 比较计算的校验和与接收到的校验和
if (checksum == data[size - 1]) {
// 校验成功
char message[] = "Checksum check successfully.";
HAL_UART_Transmit(&huart1, message, strlen(message), HAL_MAX_DELAY);
return 1; // 校验通过
} else {
// 校验失败
char message[] = "Checksum check failed.";
HAL_UART_Transmit(&huart1, message, strlen(message), HAL_MAX_DELAY);
return 0; // 校验失败
}
}

CRC校验

数据包

将信息固定帧格式打包,作为发送端数据包

  • 奇偶校验
    • 使用串口调试助手生成一个发送数据包的话,可以用电脑计算器的程序员模式异或算出最后一位奇偶校验码,取最后面两位即可。
    • 波特率动串口调试助手可以自动计算校验和。
      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
      //全局变量,数据包根据FLAG对发送数据进行打包
      int Red_Flag = 0;
      int Green_Flag = 0;
      int Yello_Flag = 0;

      void generateDataPacket(uint8_t* data) {
      // 设置初始帧头和长度
      data[0] = 0xAA; // 帧头
      data[1] = 0x09; // 数据长度(包括帧头、长度、LED控制和校验位)

      // 设置LED控制
      if (Red_Flag == 1) {
      data[2] = 0x01; // 点亮红色LED
      data[3] = 0xFF;
      } else {
      data[2] = 0x01; // 关闭红色LED
      data[3] = 0x00;
      }

      if (Yello_Flag == 1) {
      data[4] = 0x02; // 点亮黄色LED
      data[5] = 0xFF;
      } else {
      data[4] = 0x02; // 关闭黄色LED
      data[5] = 0x00;
      }

      if (Green_Flag == 1) {
      data[6] = 0x03; // 点亮绿色LED
      data[7] = 0xFF;
      } else {
      data[6] = 0x03; // 关闭绿色LED
      data[7] = 0x00;
      }

      // 计算奇偶校验位
      uint8_t parity = 0;
      for (int i = 0; i < 8; i++) {
      parity ^= data[i]; // XOR计算校验
      }
      data[8] = parity; // 将计算得出的校验位放入数据包的最后

      // 此时数据包已经封装完成,可以通过串口发送
      HAL_UART_Transmit(&huart1, data, 9, HAL_MAX_DELAY);
      }
  • 如果是和校验,则最后一位:
    1
    2
    3
    4
    5
    6
    // 计算校验和
    uint8_t checksum = 0;
    for (int i = 0; i < 8; i++) {
    checksum += data[i]; // 求和
    }
    data[8] = checksum;

接收端将数据包固定帧格式解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//串口接收、校验过程略
//例程的帧格式规定为
uint8_t valid_data[] = {
0xAA, 0x09, // 帧头和长度
0x01, 0xFF, // 点亮红色LED
0x02, 0x00, // 关闭黄色LED
0x03, 0x00, // 关闭绿色LED
0x5C // 奇偶校验位(计算其他数据的XOR)
};

void ParseDataPacket(uint8_t* data, uint16_t size) {
for (int i = 2; i < size - 1; i += 2) {
uint8_t state = (data[i + 1] == 0xFF) ? 1 : 0;
switch (data[i]) {
case 0x01: Red_Flag = state; break;
case 0x02: Yello_Flag = state; break;
case 0x03: Green_Flag = state; break;
}
}
}

蓝牙模块


stm32常用模块的函数整理
http://zoechen04616.github.io/2025/01/19/stm32常用模块的函数整理/
作者
Yunru Chen
发布于
2025年1月19日
许可协议