1+1=10

记记笔记,放松一下...

STM32系统外设小记

接前面Keil MDK 社区版小记STM32CubeMX小记,已经可以实现,让 CubeMX 生成的代码,同时兼容Keil MDK和CubeIDE两套环境:一套代码,两个IDE可以自由切换,两个人可以用不同的IDE协同工作。

只是想不通,国内各种资料为什么执着于Keil MDK这个开发环境(其社区版又不能商用)。

现在可以回到起点,了解一下STM32的系统外设了...

系统外设?

ST的 wiki 中列的4类14种:

  • 系统外设(System Peripherals):GPIO,EXTI,PWR,LPBAM,DMA
  • 基本通讯外设(Basic communication peripherals):UART,SPI,I2C,I3C
  • 定时外设(Timing peripherals):RTC,WDG,HRTIM
  • 模拟外设(Analog peripherals):DAC,ADC

列个表

模块 全称 功能 主要特点
GPIO General-Purpose Input/Output 通用输入/输出端口,用于控制和读取外部设备的状态 可配置为输入、输出、复用功能;支持上拉、下拉电阻
EXTI External Interrupt/Event Controller 外部中断/事件控制器,用于处理外部信号触发的中断 支持多种触发模式(上升沿、下降沿、双边沿);灵活的中断优先级配置
PWR Power Control 电源管理模块,用于控制微控制器的电源状态和低功耗模式 支持不同的低功耗模式(睡眠、停止、待机);电压监控和电池管理
LPBAM Low Power Background Autonomous Mode 低功耗自动模式,用于在低功耗状态下执行后台任务 允许在低功耗模式下执行特定任务,如数据传输和采集
DMA Direct Memory Access 直接存储器访问,用于在外设和内存之间快速传输数据 减少CPU负载,提高数据传输效率;支持多通道和多种传输模式
UART Universal Asynchronous Receiver/Transmitter 通用异步收发器,用于串行通信 支持全双工通信,波特率可编程;具有硬件流控制和多种错误检测机制
SPI Serial Peripheral Interface 串行外设接口,用于高速同步串行通信 支持主从模式、多从设备;全双工通信,数据传输速率高
I2C Inter-Integrated Circuit 集成电路间通信,用于短距离通信的多主多从总线 支持多主多从模式;具有从设备地址识别和7位或10位地址模式
I3C Improved Inter-Integrated Circuit 改进型集成电路间通信,I2C 的升级版 兼容 I2C;更高的速度和更低的功耗;支持动态地址分配
RTC Real-Time Clock 实时时钟,用于提供准确的时间和日期信息 支持闹钟、中断和时间戳功能;内置备份电池,掉电后仍能保持时间
WDG Watchdog Timer 看门狗定时器,用于防止系统失效或卡死 定时器溢出时触发系统复位;可编程超时时间
HRTIM High-Resolution Timer 高分辨率定时器,用于精确的时间控制和信号生成 提供高分辨率的PWM输出;支持复杂的时间和事件控制
DAC Digital-to-Analog Converter 数模转换器,用于将数字信号转换为模拟信号 支持多通道输出;分辨率和采样率可编程
ADC Analog-to-Digital Converter 模数转换器,用于将模拟信号转换为数字信号 高速采样、分辨率可调;支持多通道输入和多种触发模式

东西太多了,先了解一下其中的 GPIO、UART、SPI

GPIO

这个东西...

GPIO是什么?

GPIO(General Purpose Input/Output,通用输入/输出)是微控制器或其他集成电路上的一种通用引脚,用于与外部设备进行数字信号交互。GPIO 引脚没有特定的功能,可以通过软件配置来实现多种用途,因此称为“通用”。

先借ST官方GPIO internal peripheral一张图:

stm32-gpio-internal

对于GPIO的几种模式,可以列个表:

模式名称 宏定义 详细描述
输入模式 GPIO_Mode_IN 配置引脚为输入模式,用于读取外部信号。
输出模式 GPIO_Mode_OUT 配置引脚为输出模式,用于驱动外部设备。可以选择推挽输出(Push-pull Output)或开漏输出(Open-drain Output)。
复用功能模式 GPIO_Mode_AF 配置引脚为复用功能模式,用于连接内部外设(如 UART、I2C、SPI 等)。具体的复用功能由引脚的 AF(Alternate Function)寄存器配置。
模拟模式 GPIO_Mode_AN 配置引脚为模拟模式,用于模拟信号处理。常用于 ADC 输入或 DAC 输出。

对于输出模式PuPd:

  • 推挽输出(Push-pull Output):标准的输出模式,能够提供强驱动能力。
  • 开漏输出(Open-drain Output):适用于需要外部上拉电阻的应用,如 I2C 总线。

HAL函数

HAL库中主要的一些GPIO相关函数(具体可以查看stm32xxxx_hal_gpio.h文件)

  • 初始化和配置相关
1
2
HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin)
  • 状态读写
1
2
3
HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
  • 中断配置和处理
1
2
HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)

使用

cubemx或cubeide可以直接生成配置代码。剩下的编写读写代码就行了。单独控制一个灯闪烁,很简单:

1
2
3
4
5
6
7
8
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_2);
        HAL_Delay(1000);

    /* USER CODE END WHILE */

引脚可以通过cubemx设置别名(对应C语言的宏),那么:

1
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);

如果不使用toggle,可以直接设置值:

1
2
3
4
5
6
  while (1)
  {
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
        HAL_Delay(1000);

使用Delay延迟比较比较傻,正常应该使用定时器中断,比如配置使用CubeMX配置,TIM2定时器。而后在main.c 中定义:

1
2
3
4
5
6
7
8
9
// 定时器中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        // 切换 LED 状态
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    }
}

这是一个弱定义函数。

它能工作,另一方面是因为在stm32xxx_it.c中的中断处理函数调用HAL的函数。

1
2
3
4
void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

UART

RS232等uart串口概念简单

HAL函数

  • 初始化与配置
1
2
HAL_UART_Init(UART_HandleTypeDef *huart)
HAL_UART_DeInit(UART_HandleTypeDef *huart)
  • UART数据传输
1
2
3
4
5
6
7
8
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
  • 中断处理
1
2
3
HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

使用

初始化代码cubemx可以生成。只需关注数据传输,数据传输有三个编程模式

  • 轮询方式
  • 中断方式(IT)
  • DMA方式(DMA)

先只考虑其中的中断方式。

全局变量

main函数外,定义全局变量,代表发送和接受缓冲区

1
2
3
4
/* USER CODE BEGIN PV */
uint8_t recv_buffer[128] = "";
uint8_t send_buffer[128] = "";
/* USER CODE END PV */

接收

main函数中,在while之外,启动接收

1
2
3
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, recv_buffef, 3);
/* USER CODE END 2 */

中断处理

main函数外,定义回调函数,内部书写比较自由

1
2
3
4
5
6
7
8
9
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_2);
    memcpy(send_buffer, recv_buffer, 3);
    HAL_UART_Transmit_IT(huart, send_buffer, 3);
    HAL_UART_Receive_IT(huart, recv_buffer, 3);
}
/* USER CODE END 4 */

注意:这个函数名一定不能拼错了(Cplt是完成Complete的意思),拼错后也不会报错,没有这个函数也不会报错。因为:

在 STM32 HAL 库中,HAL_UART_RxCpltCallback 是一个弱定义(__weak)的函数。这意味着,如果用户没有显式地定义这个函数,链接器会使用库中提供的默认实现;如果用户定义了这个函数,链接器会使用用户提供的实现而不是默认的实现。

可用的回调,可以通过stm32xxxx_hal_uart.h文件查看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);        /*!< UART Tx Half Complete Callback        */
  void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Tx Complete Callback             */
  void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);        /*!< UART Rx Half Complete Callback        */
  void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Rx Complete Callback             */
  void (* ErrorCallback)(struct __UART_HandleTypeDef *huart);             /*!< UART Error Callback                   */
  void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart);         /*!< UART Abort Complete Callback          */
  void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Transmit Complete Callback */
  void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart);  /*!< UART Abort Receive Complete Callback  */
  void (* WakeupCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Wakeup Callback                  */
  void (* RxEventCallback)(struct __UART_HandleTypeDef *huart, uint16_t Pos); /*!< UART Reception Event Callback     */

  void (* MspInitCallback)(struct __UART_HandleTypeDef *huart);           /*!< UART Msp Init callback                */
  void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart);         /*!< UART Msp DeInit callback              */

SPI

SPI(Serial Peripheral Interface,串行外设接口)是一种同步串行通信协议,通常用于微控制器与各种外围设备(如传感器、存储设备、显示屏等)之间的通信。SPI 由 Motorola 公司开发,是一种全双工通信协议,具有高速、简单和灵活的特点。

四条主要信号线:

  • MISO(Master In Slave Out):主设备输入、从设备输出。这条线用于从设备向主设备发送数据。
  • MOSI(Master Out Slave In):主设备输出、从设备输入。这条线用于主设备向从设备发送数据。
  • SCLK(Serial Clock):串行时钟信号,由主设备生成,用于同步数据传输。
  • SS(Slave Select)或 CS(Chip Select):从设备选择信号。通常由主设备控制,用于选择哪个从设备进行通信。

HAL函数

和串口一样,也是支持轮询、中断、DMA三种传输方式。

  • 初始化与配置
1
HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
  • 数据传输
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);

HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size);

HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size);
  • 状态监控
1
2
HAL_SPI_StateTypeDef HAL_SPI_GetState(SPI_HandleTypeDef *hspi);
uint32_t HAL_SPI_GetError(SPI_HandleTypeDef *hspi);

C语言知识补充

之前没关注这个

关于弱定义

弱定义(Weak Symbol)是一种符号(可以是函数或变量)的声明方式,它允许在链接过程中,如果没有强定义(Strong Symbol)与之对应,则使用该弱定义。弱定义通常用于库和框架中,以提供默认实现或默认值,而用户可以通过提供自己的强定义来覆盖这些默认行为。

不同的编译器通过不同的方式支持弱定义。以下是一些常见编译器的支持方式:

  • GCC 和 Clang:使用 attribute((weak))。
  • ARM 编译器:使用 __weak。
  • MSVC:不直接支持,但可以通过链接器选项 /alternatename 实现类似功能。

参考

EE STM32