普冉(PUYA)单片机开发笔记(4): 配置通用定时器

概述

在前几篇开发笔记中,从 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/LSI
    RCC_OscInitStruct.HSIState = RCC_HSI_ON;                                // 开启HSI
    RCC_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.12MHz
    RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_24MHz;       // 配置HSI输出时钟为24MHz
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;                                // 开启 HSE
    RCC_OscInitStruct.HSEFreq = RCC_HSE_16_32MHz;                           // HSE工作频率范围16M~32M
    RCC_OscInitStruct.LSIState = RCC_LSI_OFF;                               // 关闭 LSI
    conf_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的源选择为HSI
    RCC_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;                                         // 选择 TIM16
    TimHandle.Init.Period            = 12000 - 1;                       // 自动重装载值 500us
    TimHandle.Init.Prescaler         = 1000 - 1;                        // 预分频为 1000-1,两者确定定时器中断周期为500ms
    TimHandle.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 库文件的熟悉,还会继续集(堆)成(砌)更多的功能。希望这些实验能给各位码神一些参考。谬误之处,恳请指正。

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

硬核老骆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值