概述
在前几篇开发笔记中,从 PY32F003 的一个厂家标准例程开始,实现了中断式无阻塞串口收发、对开发板 LED3 的亮/灭控制,时钟系统的初始化和时钟选择。在此基础上,今天做一下定时器的应用实验。事先考虑以下几个问题:
- 如何使用现有例程扩展开发 MCU 的其它功能
- 定时器的中断和 UART 的中断,在PY32F003 上如何协调
- 使用 PY32 MCU 的定时器和使用 STM32 有什么异同
仍然使用 PUYA 的官方开发板,使用 SEGGER J-Link 仿真器,SWD 接口,四根线,3V3-DIO-CLK-GND。板子上接有 LED,连接 PB5,灌流式,外部3V3上拉,低电平点亮,高电平熄灭。外接UART2(PA0--TX,PA1--RX)。
这一次,先做一个最简单的定时器实验:当定时器 Elapse(流逝?到点了更贴切一些) 的时候,翻转板载 LED。
使用PY32F003的外部时钟
在 main(void) 函数中使用 SystemClock_Config() 函数初始化时钟系统,并选择 HSE 作为时钟源,代码如下
/********************************************************************************************************
**函数信息 :void SystemClock_Config(void)
**功能描述 :系统时钟配置
**输入参数 :
**输出参数 :
** 备注 :
********************************************************************************************************/
HAL_StatusTypeDef SystemClock_Config(void)
{HAL_StatusTypeDef conf_res= HAL_OK;RCC_OscInitTypeDef RCC_OscInitStruct = {0};RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_HSI | RCC_OSCILLATORTYPE_LSI; // 配置时钟源HSE/HSI/LSE/LSIRCC_OscInitStruct.HSIState = RCC_HSI_ON; // 开启HSIRCC_OscInitStruct.HSIDiv = RCC_HSI_DIV1; // 不分频//RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_8MHz; // 配置HSI输出时钟为8MHz//RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_4MHz; // 配置HSI输出时钟为4MHz//RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_16MHz; // 配置HSI输出时钟为16MHz//RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_22p12MHz; // 配置HSI输出时钟为22.12MHzRCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_24MHz; // 配置HSI输出时钟为24MHzRCC_OscInitStruct.HSEState = RCC_HSE_ON; // 开启 HSERCC_OscInitStruct.HSEFreq = RCC_HSE_16_32MHz; // HSE工作频率范围16M~32MRCC_OscInitStruct.LSIState = RCC_LSI_OFF; // 关闭 LSIconf_res = HAL_RCC_OscConfig(&RCC_OscInitStruct); // 初始化RCC振荡器if (conf_res != HAL_OK) return conf_res;//初始化CPU,AHB,APB总线时钟RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1; // RCC系统时钟类型RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSE; // SYSCLK的源选择为HSIRCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // APH时钟不分频RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB时钟不分频conf_res = HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1); // 初始化RCC系统时钟// (FLASH_LATENCY_0=24M以下;// FLASH_LATENCY_1=48M)if (conf_res != HAL_OK) return conf_res;return HAL_OK;
}
注意后面的一段中的 SYSCLKSource 指定为 RCC_SYSCLKSOURCE_HSE 选择了 HSE。AHBCLKDivider 和 APB1CLKDivider 两者决定了所有定时器的时钟频率。在我的实验环境中,这两个参数都选择“不分频”,使定时器的总线频率最高。外接晶振的频率为 24MHz,这是一个重要的基础频率,决定了在定时器配置中的 Prescaler 和 Period 的取值。
测试中,发现在初始化时钟时,即使要使用 HSE,也要设置
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
如果设置成 RCC_HSI_OFF,MCU 将卡死。
由于 PY32F0003 没有 PLL,APH 和 APB 都不分频可以提高定时精度。
配置TIM16
定时周期
在 PY32F003 上,TIM16 和 TIM17 是两个通用定时器。我选用了 TIM16,配置代码如下:
TIM_HandleTypeDef TimHandle;/********************************************************************************************************
**函数信息 :void TIM16_Config(void)
**功能描述 :初始化TIM相关MSP
**输入参数 :
**输出参数 :
** 备注 :
********************************************************************************************************/
HAL_StatusTypeDef TIM16_Config(void)
{HAL_StatusTypeDef conf_res=HAL_OK;TimHandle.Instance = TIM16; // 选择 TIM16TimHandle.Init.Period = 12000 - 1; // 自动重装载值 500usTimHandle.Init.Prescaler = 1000 - 1; // 预分频为 1000-1,两者确定定时器中断周期为500msTimHandle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟不分频TimHandle.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数TimHandle.Init.RepetitionCounter = 1 - 1; // 不重复计数TimHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // TIMx ARR 缓冲conf_res = HAL_TIM_Base_Init(&TimHandle); // TIMx 初始化if ( conf_res != HAL_OK) return conf_res;return HAL_OK;
}
这段代码实现了周期为 0.5s 的定时。参照官方说明,定时周期
T = (Period+1)*(Prescaler+1)/Fclk
其中,Fclk是总线时钟频率:24M = 24000000/秒;Period=12000-1; Prescaler=1000-1,得到定时器 Elapse 周期为
T= 12000*1000/24000000=0.5(秒)
虽然 Period 和 Prescaler 这两个值的数据类型是 uin32_t,但厂家的 HAL 库文件中说明了其取值范围从 0x0000 - 0xFFFF(65535),实际上是一个 16 位无符号整数。
- 基于 24MHz 的总线时钟频率,理论上的最小定时周期是 1/24 us,但这个定时周期无实用价值。定时器的周期设定和定时器中断程序的处理逻辑所需的 CPU 耗费有关,同时还要考虑其它中断对定时器中断的嵌套所带来的额外时间耗费。
- PY32F003 的 ALU 具有单周期的加减法,乘除法是否单周期的,官方文件没有说明,只是在 PY32F040 中列出了“单周期整数除法”。既然不能确定,在编写中断服务程序的时候,不能指望 ALU 执行单周期整数乘除法。
- 寄存器操作和 GPIO 操作可以在2~6个时钟周期内完成。
- 如果要处理业务逻辑,则需要仔细地算计中断服务程序的耗时,要确保在下一个定时中断到来之前完成所有计算。毫秒级的定时,可以完成4000条以上的汇编指令,能实现相当复杂的业务逻辑了。
- AutoReloadPreload 是 ENABLE 还是 DISABLE 在这个实验中没有影响。
编写 HAL_TIM_Base_MspInit 函数
HAL_TIM_Base_MspInit 函数由 HAL_TIM_Base_Init 函数所调用,其函数原型是一个 __weak 类型,需要在应用代码中重写,函数代码如下,完成的功能在代码的注释中已写明。
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{__HAL_RCC_TIM16_CLK_ENABLE(); //使能TIM16时钟HAL_NVIC_SetPriority(TIM16_IRQn, 0, 3); //设置中断优先级HAL_NVIC_EnableIRQ(TIM16_IRQn); //使能TIM1中断
}
在 PUYA 的 HAL 库中,HAL_TIM_Base_MspInit 被分离了出来,之所以分离这个函数的目的,我猜想是可以在 HAL_TIM_Base_MspInit 中对其做差异化的初始化,例如:是否使用中断,使用的中断优先级是多少,是否使用 DMA 等等。STM32CubeIDE 的 HAL 库函数也是这么组织的。
虽然可以在 py32f0xx_hal_tim.c 中直接修改 HAL_TIM_Base_MspInit 函数,但我习惯于在应用代码中重写这类函数,因为这么做可以保持 HAL 库函数的一致性,方便移植。
编写定时器中断服务程序 HAL_TIM_PeriodElapsedCallback
代码如下,很简单,就是将板载 LED 进行翻转。定时器是 0.5s 一次,观察 LED 的明灭,就是一秒钟亮一下了。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance != TIM16) return;HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
}
顺便重申一下:除非调试和其它不得不用到的场合,在中断服务程序中,尽量不使用 printf 这类全局阻塞式的语句
封装 HAL_TIM_Base_Start_IT 函数
这个封装,把 TimHandle 变量和 main 函数彻底分离:main 函数可以不关心这个 TIM 到底是哪一个定时器了。
HAL_StatusTypeDef TIM16_Start(void)
{return HAL_TIM_Base_Start_IT(&TimHandle);
}
实验结果
完成上述代码后,F7->F8,把程序编译烧录到开发板上,得到如下的结果,正如预期所示,开发板上的 LED 每隔 1 秒钟亮起一下,明灭时长,看着是一半一半的样子。
串口收发的功能正常,在 LED 明灭的同时,在 XCOM 上点击以100ms间隔“定时发送”,MCU 可以连续正确地返回发送的字符串。这说明串口中断和定时器中断没有发生冲突。
实验尝试了将定时器的优先级设置为 0/1/2/3/4/5/6/7/8的时候,LED 的明灭看不出有停顿的现象。想来这个实验中,串口收发的数据量都很小,定时器中断里执行的指令也很少,产生中断嵌套的几率可以忽略不计。
Keil uVision 的工程项目文件组织优化
截止目前,例程中具备了几个功能了:时钟选择,GPIO初始化,UART初始化和定时器配置,今后还会增加功能。把这些初始化和业务逻辑操作都放在 main.c 中会有一些臃肿,这一次,对原来堆砌在 main.c 中的变量和函数进行了分类
- 在 Application/User 组中增加了 app_uart.c,app_timer.c,app.c 三个文件。
- main.c 中关于 UART/USART 的变量和函数都搬到了 app_uart.c 文件中,包括;
- int fputc(int ch, FILE *f);
- void Debug_Info(const char* msg);
- HAL_StatusTypeDef USART_Config(void);
- HAL_StatusTypeDef DBG_UART_Start(void);
- void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);
- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *pHUart);
- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle);
- main.c 中关于 TIMER,TIM16 的变量和函数都搬到了 app_timer.c 文件中
- HAL_StatusTypeDef TIM16_Config(void);
- HAL_StatusTypeDef TIM16_Start(void);
- void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim);
- void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
- main.c 中未分类的函数搬到了 app.c 中
- HAL_StatusTypeDef SystemClock_Config(void);
- 其它零碎的函数
main.c 只调用封装好的函数,关注逻辑和顺序,不再关心全局变量和每一个函数的实现。分离之后,main(void) 函数简洁多了,整个 main.c 文件如下所示。如果你愿意,那么 main 函数只需要 11~12 行代码就够了。
/********************************************************************************* @file main.c* @brief Main program entry.******************************************************************************* @attention** Copyright (c) 2023 CuteModem Intelligence.* All rights reserved.** This software is licensed under terms that can be found in the LICENSE file* in the root directory of this software component.* If no LICENSE file comes with this software, it is provided AS-IS.********************************************************************************//* Private includes*/
#include "main.h"
#include <stdio.h>
/* Private define ------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*/
/* Private user code ---------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*//**
* -------------------------------------------------------------------------
* @file : int main(void)
* @brief : main函数
* @param : 无
* @retval : 无限循环,无返回值
* @remark :
* -------------------------------------------------------------------------
*/
int main(void)
{HAL_Init(); // systick初始化SystemClock_Config(); // 配置系统时钟if(USART_Config() != HAL_OK) Error_Handler(); printf("[SYS_INIT] Debug port initilaized.\r\n");if(GPIO_Config() != HAL_OK) Error_Handler(); printf("[SYS_INIT] Board LED initilaized.\r\n");if(TIM16_Config() != HAL_OK) Error_Handler();printf("[SYS_INIT] Timer initialized.\r\n");if (TIM16_Start() != HAL_OK) Error_Handler();printf("[SYS_INIT] Timer started.\r\n");printf("\r\n+---------------------------------------+""\r\n| PY32F003 MCU is ready. |""\r\n+---------------------------------------+""\r\n");if (DBG_UART_Start() != HAL_OK) Error_Handler();while (1){/*** For testing GPIO output* 2023-11-24* Hard coder Luoyuan*/
#if(0)// Toggle LED3 in TIM16 IT service procedure instead.HAL_Delay(1000);HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
#endif}
}/**
* -------------------------------------------------------------------------
* @brief : void Error_Handler(void)
* @detail : 错误陷阱函数,提示错误,然后死循环
* @param : 无
* @retval : 无
* @remark :
* -------------------------------------------------------------------------
*/
void Error_Handler(void)
{Debug_Info("[__ERROR_] System halt.");while (1) {}
}#ifdef USE_FULL_ASSERT
/*** @brief Reports the name of the source file and the source line number* where the assert_param error has occurred.* @param file: pointer to the source file name* @param line: assert_param error line source number* @retval None*/
void assert_failed(uint8_t *file, uint32_t line)
{/* User can add his own implementation to report the file name and line number,tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
}
#endif /* USE_FULL_ASSERT */
这里所调用的函数,要在 main.h 文件中定义好。实验所用的 main.h 代码如下,除了众多的注释以外,可运行代码也就30行的样子。
/********************************************************************************* @file main.h* @author MCU Application Team* @Version V1.0.0* @Date* @brief Header for main.c file.* This file contains the common defines of the application.*******************************************************************************//* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __MAIN_H
#define __MAIN_H#ifdef __cplusplus
extern "C"
{
#endif/* Includes ------------------------------------------------------------------*/
#include "py32f0xx_hal.h"
#include "py32f003xx_Start_Kit.h"
#include <stdbool.h>/* Exported functions prototypes ---------------------------------------------*/
HAL_StatusTypeDef SystemClock_Config(void);
HAL_StatusTypeDef GPIO_Config(void);
HAL_StatusTypeDef USART_Config(void);
HAL_StatusTypeDef DBG_UART_Start(void);
HAL_StatusTypeDef TIM16_Config(void);
HAL_StatusTypeDef TIM16_Start(void);void Debug_Info(const char* msg);void MX_DMA_Init(void);
void MX_ADC1_Init(void);
void Error_Handler(void);#define TEST_PORT USART2 // USART1
#define DEFAULT_UART_CFG (4) // 115200
// #define DEF_OVERSAMPLING_8
// #define IDLE_TEST// USART1
#define USARTx USART1
#define USARTx_CLK_ENABLE() __HAL_RCC_USART1_CLK_ENABLE()
#define USARTx_RX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define USARTx_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()#define USARTx_FORCE_RESET() __HAL_RCC_USART1_FORCE_RESET()
#define USARTx_RELEASE_RESET() __HAL_RCC_USART1_RELEASE_RESET()/* Definition for USARTx Pins */
#define USARTx_TX_PIN GPIO_PIN_2
#define USARTx_TX_GPIO_PORT GPIOA
#define USARTx_TX_AF GPIO_AF1_USART1
#define USARTx_RX_PIN GPIO_PIN_3
#define USARTx_RX_GPIO_PORT GPIOA
#define USARTx_RX_AF GPIO_AF1_USART1/* Definition for USARTx's NVIC */
#define USARTx_IRQn USART1_IRQn
#define USARTx_IRQHandler USART1_IRQHandler//#define TIMx TIM16
//#define TIMx_CLK_ENABLE() __HAL_RCC_TIM16_CLK_ENABLE()
//#define TIMx_IRQn TIM16_IRQn/* Size of Trasmission buffer */
#define TXBUFFERSIZE (COUNTOF(aTxBuffer) - 1)
/* Size of Reception buffer */
#define RXBUFFERSIZE TXBUFFERSIZE/* Exported macro ------------------------------------------------------------*//* Private defines -----------------------------------------------------------*/#ifdef __cplusplus
}
#endif#endif /* __MAIN_H */
按照酱紫的方法,在不改变项目文件组织框架的基础上,可以将更多的功能集成到这个项目中,main.c 和 main() 函数体保持简洁的结构和越来越明晰的业务逻辑。
总结(踩坑记)
PY32F0xx 的厂家例程包的设计是每一个例程“独立”的,这给我的实验中增加功能带来了一些困惑。每一个例程中,在 Application/User 组里的 py32f0xx_hal_msp.c 的内容都是不同的,在这个文件中定义了例程所需要的 HAL_xxx_MspInit(),例如 HAL_MspInit(),HAL_TIM_Base_MspInit() 函数等等,反正是例程所需要的 MspInit 都在 py32f0xx_hal_msp.c 文件中。这种文件组织让我好顿困惑,经过从 main() 函数的 HAL_Init,HAL_TIM_Base_Init 函数中无数的 F12,终于明白了厂家例程的这种做法。这个小坑耗费了我不少的时间去寻找解决之道。这里分享给各位码神,勿要再次入坑。
如果要用到中断的话,一定要在 py32f0xx_it.c 的 xxx_IRQHandler 函数中重定向 HAL_xxx_IRQHandler(&handler),就像我的实验中用到了 UART 中断,就要将 USART1_IRQHandler 函数重定向到 HAL_UART_Handler(&UartHandler);还用到了 TIM16 中断,就要将 TIM16_IRQHandler 重定向到 HAL_TIM_IRQHandler&TimHandler)。如果没有这些重定向,中断的Enable将会卡死。
通过这些功能的集成("堆砌"而已,稍稍给自个儿整的高大上一丢丢 ;),发现 PY32F0xx 的 HAL 库函数和 STM32F0xx 系列的还是有一些差异的。好在通过学习厂家例程,其中绝大多数的指令是兼容的,而对寄存器、USART 和定时器的底层控制方法是“几乎”完全相同的,我想这是因为都是采用相同的 Cortex M0+ 内核的缘故吧,内核一样,底层控制肯定都一样的。
随着对 PUYA HAL 库文件的熟悉,还会继续集(堆)成(砌)更多的功能。希望这些实验能给各位码神一些参考。谬误之处,恳请指正。