AT32F403A标准库学习
AT32F403A标准库学习
与 STM32F4 的 LL 库对照学习
0.系统时钟配置和嘀嗒定时器
时钟配置
和 STM32F4系统时钟配置[1]差不多,下面图中没有开 RTC,只用了高速外部晶振(HEXT)。
当外部晶振变化时,要更改at32f403a_407_conf.h文件中的HEXT_VALUE确保和自己的晶振大小一样。!!!
系统复位以后,系统时钟使用 HICK(高速内部时钟) 时钟作为默认时钟。系统时钟可在 HICK 振荡器时钟、 HEXT 振荡器时钟和 PLL 时钟之间进行灵活切换,只有当目标时钟源稳定后,系统时钟切换才会发生。当 HICK 振荡器时钟直接作为系统时钟或间接通过 PLL 作为系统时钟时,它将无法被停止。

/**
**************************************************************************
* @file at32f403a_407_clock.c
* @brief system clock config program
**************************************************************************
* Copyright notice & Disclaimer
*
* The software Board Support Package (BSP) that is made available to
* download from Artery official website is the copyrighted work of Artery.
* Artery authorizes customers to use, copy, and distribute the BSP
* software and its related documentation for the purpose of design and
* development in conjunction with Artery microcontrollers. Use of the
* software is governed by this copyright notice and the following disclaimer.
*
* THIS SOFTWARE IS PROVIDED ON "AS IS" BASIS WITHOUT WARRANTIES,
* GUARANTEES OR REPRESENTATIONS OF ANY KIND. ARTERY EXPRESSLY DISCLAIMS,
* TO THE FULLEST EXTENT PERMITTED BY LAW, ALL EXPRESS, IMPLIED OR
* STATUTORY OR OTHER WARRANTIES, GUARANTEES OR REPRESENTATIONS,
* INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.
*
**************************************************************************
*/
/* includes ------------------------------------------------------------------*/
#include "at32f403a_407_clock.h"
/**
* @brief system clock config program
* @note the system clock is configured as follow:
* system clock (sclk) = hext * pll_mult
* system clock source = HEXT_VALUE
* - hext = 12000000
* - sclk = 240000000
* - ahbdiv = 1
* - ahbclk = 240000000
* - apb1div = 2
* - apb1clk = 120000000
* - apb2div = 2
* - apb2clk = 120000000
* - pll_mult = 20
* - pll_range = GT72MHZ (greater than 72 mhz)
* @param none
* @retval none
*/
void system_clock_config(void)
{
/* reset crm */
crm_reset();
/* enable hext */
crm_clock_source_enable(CRM_CLOCK_SOURCE_HEXT, TRUE);
/* wait till hext is ready */
while(crm_hext_stable_wait() == ERROR)
{
}
/* config pll clock resource */
crm_pll_config(CRM_PLL_SOURCE_HEXT, CRM_PLL_MULT_20, CRM_PLL_OUTPUT_RANGE_GT72MHZ);
/* enable pll */
crm_clock_source_enable(CRM_CLOCK_SOURCE_PLL, TRUE);
/* wait till pll is ready */
while(crm_flag_get(CRM_PLL_STABLE_FLAG) != SET)
{
}
/* config ahbclk */
crm_ahb_div_set(CRM_AHB_DIV_1);
/* config apb2clk */
crm_apb2_div_set(CRM_APB2_DIV_2);
/* config apb1clk */
crm_apb1_div_set(CRM_APB1_DIV_2);
/* enable auto step mode */
crm_auto_step_mode_enable(TRUE);
/* select pll as system clock source */
crm_sysclk_switch(CRM_SCLK_PLL);
/* wait till pll is used as system clock source */
while(crm_sysclk_switch_status_get() != CRM_SCLK_PLL)
{
}
/* disable auto step mode */
crm_auto_step_mode_enable(FALSE);
/* update system_core_clock global variable */
system_core_clock_update();
}
嘀嗒定时器
Cortex-M 处理器内集成了一个小型的名为 SysTick(系统节拍)的定时器,它属于 NVIC 的一部分,且可以产生 SysTick 异常(异常类型#15)。SysTick 为简单的向下计数的 24 位计数器,可以使用处理器时钟或外部参考时钟(通常是片上时钟源)。
在现代操作系统中,需要一个周期性的中断来定期触发 OS 内核,如用于任务管理和上下文切换,处理器也可以在不同时间片内处理不同任务。处理器设计还需要确保运行在非特权等级的应用任务无法禁止该定时器,否则任务可能会禁止 SysTick 定时器并锁定整个系统。
之所以在处理器内增加一个定时器,是为了提高软件的可移植性。由于所有的 Cortex-M 处理器都具有相同的 SysTick 定时器,为一种 Cortex-M3/M4 微控制器实现的 OS 也能适用于其他的 Cortex-M3/M4 微控制器。
若应用中不需要使用 OS,SysTick 定时器可用作简单的定时器外设,用以产生周期性中断、延时或时间测量。
| CMSIS-Core 符号 | 寄存器 | 地址 |
|---|---|---|
| CTRL | SysTick 控制和状态寄存器 | 0xE000E010 |
| LOAD | SysTick 重装载值寄存器 | 0xE000E014 |
| VAL | SysTick 当前值寄存器 | 0xE000E018 |
| CALIB | SysTick 校准值寄存器 | 0xE000E01C |

只有当计数器从 1 降到 0 触发
使用嘀嗒定时器实现延时
- 设置嘀嗒定时器的时钟源,这里设置的是 AHBCLK,并且没有分频,也就是 240MHz
- fac_us 这个值为系统时钟除以 1M,即 240,那么以 240MHz 计数,计 240 次就是 1us,fac_ms 同理
- 这里延时采用的是查询的方式,简单粗暴,先把值装入 LOAD 中,然后把 VAL 清 0(会同时清掉计数标志位)
- 使能嘀嗒计时器,一直查询计数标志位是否置位实现延时
#define STEP_DELAY_MS 50
static __IO uint32_t fac_us;
static __IO uint32_t fac_ms;
/**
* @brief initialize delay function
* @param none
* @retval none
*/
void delay_init()
{
/* configure systick */
systick_clock_source_config(SYSTICK_CLOCK_SOURCE_AHBCLK_NODIV);
fac_us = system_core_clock / (1000000U);
fac_ms = fac_us * (1000U);
}
/**
* @brief inserts a delay time.
* @param nus: specifies the delay time length, in microsecond.
* @retval none
*/
void delay_us(uint32_t nus)
{
uint32_t temp = 0;
SysTick->LOAD = (uint32_t)(nus * fac_us);
SysTick->VAL = 0x00;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
do
{
temp = SysTick->CTRL;
} while ((temp & 0x01) && !(temp & (1 << 16)));
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL = 0x00;
}
/**
* @brief inserts a delay time.
* @param nms: specifies the delay time length, in milliseconds.
* @retval none
*/
void delay_ms(uint16_t nms)
{
uint32_t temp = 0;
while (nms)
{
if (nms > STEP_DELAY_MS)
{
SysTick->LOAD = (uint32_t)(STEP_DELAY_MS * fac_ms);
nms -= STEP_DELAY_MS;
}
else
{
SysTick->LOAD = (uint32_t)(nms * fac_ms);
nms = 0;
}
SysTick->VAL = 0x00;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
do
{
temp = SysTick->CTRL;
} while ((temp & 0x01) && !(temp & (1 << 16)));
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL = 0x00;
}
}
/**
* @brief inserts a delay time.
* @param sec: specifies the delay time, in seconds.
* @retval none
*/
void delay_sec(uint16_t sec)
{
uint16_t index;
for (index = 0; index < sec; index++)
{
delay_ms(500);
delay_ms(500);
}
}
值得注意的是 system_core_clock 这个全局变量,声明时给的值时 8000000,因为系统内部高速时钟为 48M,分频到 8M,但在时钟配置 void system_clock_config(void); 这个函数中的 system_core_clock_update(); 这个函数把他更新到新配置的频率。
1.点灯
配置流程和 STM32GPIO配置[2]的基本一样,但有一个不同点,AT32 这个配置结构体具体数据之前总会默认初始化下结构体
- 声明结构体
- 开 IO 端口的时钟
- 默认初始化结构体
- 配置结构体
- 把结构体装入初始化函数
gpio_init_type gpio_init_struct;
crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);
gpio_default_para_init(&gpio_init_struct);
gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
gpio_init_struct.gpio_mode = GPIO_MODE_OUTPUT;
gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL;
gpio_init_struct.gpio_pins = GPIO_PINS_0 | GPIO_PINS_1 | GPIO_PINS_2;
gpio_init_struct.gpio_pull = GPIO_PULL_NONE;
gpio_init(GPIOA, &gpio_init_struct);
#include "at32f403a_407_clock.h"
#include "SEGGER_RTT.h"
uint8_t t = 0;
int main(void)
{
system_clock_config();
delay_init();
gpio_init_type gpio_init_struct;
crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);
gpio_default_para_init(&gpio_init_struct);
gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
gpio_init_struct.gpio_mode = GPIO_MODE_OUTPUT;
gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL;
gpio_init_struct.gpio_pins = GPIO_PINS_0 | GPIO_PINS_1 | GPIO_PINS_2;
gpio_init_struct.gpio_pull = GPIO_PULL_NONE;
gpio_init(GPIOA, &gpio_init_struct);
SEGGER_RTT_ConfigUpBuffer(0, NULL, NULL, 0, SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL);
while (1)
{
gpio_bits_reset(GPIOA, GPIO_PINS_1);
gpio_bits_set(GPIOA, GPIO_PINS_0);
gpio_bits_set(GPIOA, GPIO_PINS_2);
SEGGER_RTT_printf(0, "%s%sLED times:%d\n", RTT_CTRL_BG_BLACK, RTT_CTRL_TEXT_BRIGHT_GREEN, t);
// SEGGER_RTT_printf(0, "LED: %d\n", t);
t++;
delay_ms(500);
gpio_bits_set(GPIOA, GPIO_PINS_1);
gpio_bits_reset(GPIOA, GPIO_PINS_0);
gpio_bits_reset(GPIOA, GPIO_PINS_2);
delay_ms(500);
}
}
2.外部中断(EXINT)
- 声明一个外部中断配置的结构体
- 开
IOMUX的时钟,这个时钟和 STM32F4 的SYSCFG差不多,主要给引脚重映射和外部中断使用 - 如用 GPIO 作为中断源,则配置 GPIO,然后选择 GPIO 当作中断源
- 默认初始化外部中断结构体
- 配置结构体
- 将结构体填入
exint_init函数 - 设置中断优先级分组(设置了一次就不用再设置了,之前设置过则跳过这步,一次都没设置则按默认的来:0 位给抢占优先级,4 位给响应优先级)
- 开启外部中断并填入优先级别
void exint_config(void)
{
gpio_init_type gpio_init_struct;
exint_init_type exint_init_strcut;
crm_periph_clock_enable(CRM_IOMUX_PERIPH_CLOCK, TRUE);
crm_periph_clock_enable(CRM_GPIOC_PERIPH_CLOCK, TRUE);
// 配置按键
gpio_default_para_init(&gpio_init_struct);
gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
gpio_init_struct.gpio_mode = GPIO_MODE_INPUT;
gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL;
gpio_init_struct.gpio_pins = GPIO_PINS_13;
gpio_init_struct.gpio_pull = GPIO_PULL_UP;
gpio_init(GPIOC, &gpio_init_struct);
// 配置外部中断
gpio_exint_line_config(GPIO_PORT_SOURCE_GPIOC, GPIO_PINS_SOURCE13); // 选择中断源,仅GPIO作为中断源需要配置此步
exint_default_para_init(&exint_init_strcut);
exint_init_strcut.line_enable = TRUE;
exint_init_strcut.line_mode = EXINT_LINE_INTERRUPUT;
exint_init_strcut.line_polarity = EXINT_TRIGGER_FALLING_EDGE;
exint_init_strcut.line_select = EXINT_LINE_13;
exint_init(&exint_init_strcut);
nvic_priority_group_config(NVIC_PRIORITY_GROUP_2);
nvic_irq_enable(EXINT15_10_IRQn, 1, 1);
}
3.通用同步异步收发器(USART)
USART 配置流程如下
- 声明一个 GPIO 的结构体
- 开用到的 GPIO 的时钟和 USART 的时钟
- 配置 GPIO 和上面的一样
- 配置串口相关参数
- 使能接收和发送(如用 DMA 再使能 DMA 发送和接收)
- 设置 USART 中断优先级
- 打开 USART 的中断,好几种,具体用哪个开哪个
- 使能 USART 外设
gpio_init_type gpio_init_struct;
crm_periph_clock_enable(CRM_USART1_PERIPH_CLOCK, TRUE);
crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);
// 引脚配置 PA9-->TX PA10-->RX
gpio_default_para_init(&gpio_init_struct);
// TX引脚配置
gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
gpio_init_struct.gpio_mode = GPIO_MODE_MUX;
gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL;
gpio_init_struct.gpio_pins = GPIO_PINS_9;
gpio_init_struct.gpio_pull = GPIO_PULL_NONE;
gpio_init(GPIOA, &gpio_init_struct);
// RX引脚配置
gpio_init_struct.gpio_mode = GPIO_MODE_INPUT; //
gpio_init_struct.gpio_pins = GPIO_PINS_10;
gpio_init(GPIOA, &gpio_init_struct);
// 配置串口一
usart_init(USART1, 115200, USART_DATA_8BITS, USART_STOP_1_BIT);
usart_transmitter_enable(USART1, TRUE);
usart_receiver_enable(USART1, TRUE);
usart_dma_transmitter_enable(USART1, TRUE);
usart_dma_receiver_enable(USART1, TRUE);
nvic_irq_enable(USART1_IRQn, 1, 0);
usart_interrupt_enable(USART1, USART_IDLE_INT, TRUE);
usart_enable(USART1, TRUE);
为了发大数据和减少 CPU 的负担,用打工人 DMA 来帮忙,配置如下
USART 发送 DMA 配置流程
- 选择 DMA 传输通道: 在 DMA 章节 DMA 通道映射表中选择用于当前所用 USART 的 DMA 通道。
- 配置 DMA 传输 目标地址 : 在 DMA 控制寄存器中 DMA 传输目的地址位写当前所使用的
- USART 的数据寄存器( USART_DT) 地址,DMA 将会在接收到发送请求后将代发送的数据写入该地址。
- 配置 DMA 传输源地址: 在 DMA 控制寄存器中 DMA 传输源地址位写入代发送数据存放的地址,DMA 将会在接收到发送请求后将该地址内的数据写入到目标地址中, 即写入到当前所使用的 USART 的数据寄存器( USART_DT)中。
- 配置 DMA 传输字节个数: 在 DMA 控制寄存器相关位置配置期望传输的字节个数
- 配置 DMA 传输通道优先级: 在 DMA 控制寄存器相关位置配置当前所使用通道的 USART 的 DMA 传输通道优先级。
- 配置 DMA 中断产生时机: 在 DMA 控制寄存器相关位置配置是在传输完成或传输完成一半时产生 DMA 中断。
- 使能 DMA 传输通道: 在 DMA 控制寄存器相关位置使能当前所选用的 DMA 通道
USART 接收 DMA 配置流程
- 选择 DMA 传输通道: 在 DMA 章节 DMA 通道映射表中选择用于当前所用 USART 的 DMA 通道。
- 配置 DMA 传输目标地址: 在 DMA 控制寄存器中 DMA 传输目的地址位写入期望存放接收数据 的 地 址 , DMA 将 会 在 接 收 到 接 收 请 求 后 , 将 当 前 所 使 用 的 USART 的 数 据 寄 存 器( USART_DT) 中的数据存放在目的地址中。
- 配置 DMA 传输源地址: 在 DMA 控制寄存器中 DMA 传输源地址位写入当前所使用的 USART 的数据寄存器( USART_DT) 的地址,DMA 将会在接收到接收请求后将该地址内的数据写入到目标地址中, 即写入到期望存放接收数据的地址。
- 配置 DMA 传输字节个数: 在 DMA 控制寄存器相关位置配置期望传输的字节个数
- 配置 DMA 传输通道优先级: 在 DMA 控制寄存器相关位置配置当前所使用通道的 USART 的 DMA 传输通道优先级。
- 配置 DMA 中断产生时机: 在 DMA 控制寄存器相关位置配置是在传输完成或传输完成一半时产生 DMA 中断。
- 使能 DMA 传输通道: 在 DMA 控制寄存器相关位置使能当前所选用的 DMA 通道
dma_init_type dma_init_struct;
crm_periph_clock_enable(CRM_DMA1_PERIPH_CLOCK, TRUE);
// 配置DMA通道4,TX
dma_reset(DMA1_CHANNEL4); // CHEN为0才能配置
dma_default_para_init(&dma_init_struct);
dma_init_struct.buffer_size = 0;
dma_init_struct.direction = DMA_DIR_MEMORY_TO_PERIPHERAL;
dma_init_struct.loop_mode_enable = FALSE;
dma_init_struct.memory_base_addr = (uint32_t)usart_rx_tx_struct->tx_buffer;
dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_BYTE;
dma_init_struct.memory_inc_enable = TRUE;
dma_init_struct.peripheral_base_addr = (uint32_t)&USART1->dt;
dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_BYTE;
dma_init_struct.peripheral_inc_enable = FALSE;
dma_init_struct.priority = DMA_PRIORITY_MEDIUM;
dma_init(DMA1_CHANNEL4, &dma_init_struct);
// 开启传输完成中断
// dma_interrupt_enable(DMA1_CHANNEL4, DMA_FDT_INT, TRUE); //不需要中断
// nvic_irq_enable(DMA1_Channel4_IRQn, 1, 0);
// 配置DMA通道5,RX
dma_reset(DMA1_CHANNEL5);
dma_default_para_init(&dma_init_struct);
dma_init_struct.buffer_size = RX_BUFFER_SIZE;
dma_init_struct.direction = DMA_DIR_PERIPHERAL_TO_MEMORY;
dma_init_struct.loop_mode_enable = FALSE;
dma_init_struct.memory_base_addr = (uint32_t)usart_rx_tx_struct->rx_buffer;
dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_BYTE;
dma_init_struct.memory_inc_enable = TRUE;
dma_init_struct.peripheral_base_addr = (uint32_t)&USART1->dt;
dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_BYTE;
dma_init_struct.peripheral_inc_enable = FALSE;
dma_init_struct.priority = DMA_PRIORITY_MEDIUM;
dma_init(DMA1_CHANNEL5, &dma_init_struct);
// 开启传输完成中断
// dma_interrupt_enable(DMA1_CHANNEL5, DMA_FDT_INT, TRUE); //不需要中断
// nvic_irq_enable(DMA1_Channel5_IRQn, 1, 0);
// 开启DMA
dma_channel_enable(DMA1_CHANNEL4, TRUE);
dma_channel_enable(DMA1_CHANNEL5, TRUE);
几个标志位理解
USART_TDC_FLAG 发送数据完成标志位和 USART_TDBE_FLAG 发送缓冲器空区别
发送流程(如下图用的 STM32 的,AT32 的参考手册写的不太行):软件写入的值会先存储在发送数据缓冲器(TDR)中,当发送移位寄存器为空时, USART 会将发送数据缓冲器中的值移入到发送移位寄存器, USART 发送器将以 LSB 的方式将发送移位寄存器中的数据从 TX 脚输出

USART_TDBE_FLAG:当发送缓冲器为空,可以再次写入数据时, 该位被硬件置起。对 USART_DT 的写操作,将清零该位。此时,数据可能还没发送完成。
USART_TDC_FLAG:当发送数据完成,该位被硬件置起,由软件将其清零。换句话说,如果最后一次发送到数据缓冲区的数据完成了从移位寄存器到信号线 TX 时,才置 1,表示数据发送完成,也就是说,这个标志位真正表示数据发送完成。
DMA 在串口中使用注意项

- 检查标志位 TDC 标志(STM32 中为 TC)来确保 USART 的发送完成
- 任何对 DMA 的操作都要等 CHEN(通道使能位)复位才能进行操作,不然可能会卡死
貌似鲁棒的DMA串口收发
4.定时器(TIMER)
基本定时器
基本定时器(TMR6 和 TMR7)包含一个 16 位向上计数器以及对应的控制逻辑, 没有外部 I/O 接入。 可用于简单的定时功能以及为 DAC 提供时钟。
AT32F403A 定时器最高频率 240MHz 即系统最高频率,定时器挂在 APB1 和 APB2 桥接器上,APB1 和 APB2 总线最高频率为 120MHz,经过二倍频后为 240MHz。
配置流程如下:
- 开启基本定时器时钟
- 配置定时器的周期值和分频系数
- 设置计数方向,基本定时器只有向上计数模式
- 配置溢出中断
- 使能基本定时器外设
/**
* @brief 基本定时器配置,先开时钟,设置定时器的周期和分频系数,设置计数模式,
* 使能溢出中断,配置中断优先级,开启定时器外设
*
*/
void base_timer_config(void)
{
crm_periph_clock_enable(CRM_TMR6_PERIPH_CLOCK, TRUE);
tmr_base_init(TMR6, 9999, 239);
tmr_cnt_dir_set(TMR6, TMR_COUNT_UP);
tmr_interrupt_enable(TMR6, TMR_OVF_INT, TRUE);
nvic_priority_group_config(NVIC_PRIORITY_GROUP_2);
nvic_irq_enable(TMR6_GLOBAL_IRQn, 1, 1);
tmr_counter_enable(TMR6, TRUE);
}
void TMR6_GLOBAL_IRQHandler(void)
{
static uint16_t s_count = 0;
if (tmr_flag_get(TMR6, TMR_OVF_FLAG) != RESET)
{
SEGGER_RTT_printf(0, "%d", s_count);
s_count = !s_count;
tmr_flag_clear(TMR6, TMR_OVF_FLAG);
}
}
通用定时器

输入捕获
此模式下,当选中的触发信号被检测到,通道寄存器(TMRx_CxDT)记录当前计数器计数值,并将捕获比较中断标志位(CxIF)置 1,若已使能通道中断(CxIEN)、通道 DMA 请求(CxDEN)则产生相应的中断和 DMA 请求。若在 CxIF 置 1 后检测到触发信号, 将产生捕获溢出事件,TMRx_CxDT 会使用当前计数器计数值覆盖之前记录的计数器计数值, 同时通道再捕获标志位(CxRF) 置 1。
配置流程:
- 声明 GPIO 结构体和定时器输入设置结构体
- 开 GPIO 时钟和定时器时钟
- 配置定时器对应的 GPIO 引脚
- 配置定时器的基本设置
- 配置定时器输入结构体
- 开启定时器溢出中断和捕获中断
- 配置中断优先级,使能中断
- 开启定时器外设

黄色部分对应 TMR_CC_CHANNEL_MAPPED_DIRECT,红色部分对应 TMR_CC_CHANNEL_MAPPED_INDIRECT
void timer_input_capture_config(void)
{
gpio_init_type gpio_init_strut = {0};
tmr_input_config_type tmr_input_config_struct = {0};
crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);
crm_periph_clock_enable(CRM_TMR3_PERIPH_CLOCK, TRUE);
// 配置输入捕获的引脚TM3 通道2 PA7
gpio_init_strut.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
gpio_init_strut.gpio_mode = GPIO_MODE_INPUT;
gpio_init_strut.gpio_out_type = GPIO_OUTPUT_PUSH_PULL;
gpio_init_strut.gpio_pins = GPIO_PINS_7;
gpio_init_strut.gpio_pull = GPIO_PULL_NONE;
gpio_init(GPIOA, &gpio_init_strut);
tmr_base_init(TMR3, OVER_FLOW_VALUE_16BIT, 239);
tmr_cnt_dir_set(TMR3, TMR_COUNT_UP);
// 配置TIMER3通道2
tmr_input_config_struct.input_channel_select = TMR_SELECT_CHANNEL_2; // 输入通道选择通道2
tmr_input_config_struct.input_filter_value = 0; // 输入滤波,一般不用,这个后续查
tmr_input_config_struct.input_mapped_select = TMR_CC_CHANNEL_MAPPED_DIRECT; // 这边PWM模式需要占用两个通道寄存器,看图
tmr_input_config_struct.input_polarity_select = TMR_INPUT_RISING_EDGE; // 触发极性,在输入捕获模式下触发极性有上升沿、下降沿和两个边沿都触发
tmr_input_channel_init(TMR3, &tmr_input_config_struct, TMR_CHANNEL_INPUT_DIV_1); //输入分频系数为1,即不分频
tmr_interrupt_enable(TMR3, TMR_C2_INT, TRUE); //开启通道2的中断
tmr_interrupt_enable(TMR3, TMR_OVF_INT, TRUE);
nvic_priority_group_config(NVIC_PRIORITY_GROUP_2);
nvic_irq_enable(TMR3_GLOBAL_IRQn, 1, 1);
tmr_counter_enable(TMR3, TRUE);
}

STM32 参考手册中的滤波解释,看得有点不太懂
捕获实现思路:检查到触发将定时器的值放到数组中,若在第二个触发出现前期间有多次溢出则记录溢出的次数,第二次进入捕获中断将该值加上溢出中断乘以溢出值减去第一次捕获的值
void timer_input_capture_irq_process(capture_data_type *capture_data_struct)
{
if (tmr_flag_get(TMR3, TMR_OVF_FLAG) != RESET)
{
tmr_flag_clear(TMR3, TMR_OVF_FLAG);
if (capture_data_struct->capture_state == CAPTURE_START)
{
capture_data_struct->overflow_count++;
}
else if (capture_data_struct->capture_state == CAPTURE_STOP)
{
capture_data_struct->overflow_count = 0;
}
}
if (tmr_flag_get(TMR3, TMR_C2_FLAG) != RESET)
{
tmr_flag_clear(TMR3, TMR_C2_FLAG);
if (capture_data_struct->capture_state == CAPTURE_STOP && capture_data_struct->overflow_count == 0)
{
capture_data_struct->capture_data[0] = tmr_channel_value_get(TMR3, TMR_SELECT_CHANNEL_2);
capture_data_struct->capture_state = CAPTURE_START;
}
else if (capture_data_struct->capture_state == CAPTURE_START)
{
capture_data_struct->capture_data[1] = tmr_channel_value_get(TMR3, TMR_SELECT_CHANNEL_2);
if (capture_data_struct->overflow_count == 0)
{
capture_data_struct->capture_result = capture_data_struct->capture_data[1] - capture_data_struct->capture_data[0];
}
else if (capture_data_struct->overflow_count != 0)
{
capture_data_struct->capture_result = OVER_FLOW_VALUE_16BIT * capture_data_struct->overflow_count + capture_data_struct->capture_data[1] - capture_data_struct->capture_data[0];
}
capture_data_struct->capture_state = CAPTURE_STOP;
SEGGER_RTT_printf(0, "%d\n", capture_data_struct->capture_result);
}
}
}
void TMR3_GLOBAL_IRQHandler(void)
{
extern capture_data_type capture_data_struct;
timer_input_capture_irq_process(&capture_data_struct);
}
PWM 输入捕获
PWM 输入模式适用于通道 1 和 2,要使用此模式,需要将 C1IN 和 C2IN 映射到同一 TMRx_CHx,并且通道 1 或 2 的 CxIFPx 配置成触发次定时器控制器复位。

void pwm_input_config(void)
{
gpio_init_type gpio_init_struct;
tmr_input_config_type tmr_input_config_struct;
crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);
crm_periph_clock_enable(CRM_TMR3_PERIPH_CLOCK, TRUE);
gpio_default_para_init(&gpio_init_struct);
gpio_init_struct.gpio_pins = GPIO_PINS_7;
gpio_init_struct.gpio_pull = GPIO_PULL_NONE;
gpio_init_struct.gpio_mode = GPIO_MODE_INPUT;
gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
gpio_init(GPIOA, &gpio_init_struct);
tmr_base_init(TMR3, 0xFFFF, 239);
tmr_cnt_dir_set(TMR3, TMR_COUNT_UP);
tmr_input_default_para_init(&tmr_input_config_struct);
tmr_input_config_struct.input_channel_select = TMR_SELECT_CHANNEL_2;
tmr_input_config_struct.input_filter_value = 0;
tmr_input_config_struct.input_mapped_select = TMR_CC_CHANNEL_MAPPED_DIRECT;
tmr_input_config_struct.input_polarity_select = TMR_INPUT_RISING_EDGE;
tmr_pwm_input_config(TMR3, &tmr_input_config_struct, TMR_CLOCK_DIV1);
tmr_trigger_input_select(TMR3, TMR_SUB_INPUT_SEL_C2DF2); // 次定时器输入选择(Subordinate TMR input selection)用于次定时器的输入选择。
tmr_sub_mode_select(TMR3, TMR_SUB_RESET_MODE); // 选择次定时器为复位模式
tmr_sub_sync_mode_set(TMR3, TRUE); // 该位开启后,主次定时器可实现高度同步,同步中从模式有复位模式、挂起模式、触发模式
tmr_counter_enable(TMR3, TRUE); // 使能定时器
nvic_priority_group_config(NVIC_PRIORITY_GROUP_2);
nvic_irq_enable(TMR3_GLOBAL_IRQn, 1, 0);
// tmr_interrupt_enable(TMR3, TMR_OVF_INT, TRUE);
tmr_interrupt_enable(TMR3, TMR_C2_INT, TRUE);
}
图片中的配置时配置通道 1 的输入信号作为触发信号,通道 1 的寄存器(C1DT)储存 PWM 的周期,通道 2 的寄存器(C2DT)用来储存 PWM 的占空比,(代码中的配置)若配置通道 2 的输入信号作为触发信号,则通道 2 的寄存器储存的为周期,通道 1 的寄存器为占空比。

通道 1 输入信号的上升沿会触发捕获并将捕获值存储到 C1DT 寄存器,同时通道 1 输入信号上升沿复位计数器,并进中断,这时 C1DT 和 C2DT 都为 0。 通道 1 输入信号下降沿触发捕获并将捕获值存储到 C2DT 寄存器,再次进中断。通道 1 输入信号上升沿触发捕获并将值储存到 C1DT,再次进中断,一共进三次中断。通道 1 输入信号的周期可通过 C1DT 计算,占空比可通过 C2DT 计算。循环往复。
编码器模式
编码器模式下需提供两组输入信号 TMRx_CH1 和 TMRx_CH2(只能用通道 1 和通道 2,用了这个模式后,通道 3 和 4 就不能用了),根据一组输入信号电平值,计数器在另一组输入信号边沿向上或向下计数。计数方向由 OWCDIR 值指示。

编码器模式 A: SMSEL=3’b001,计数器在 C1IFP1 边沿计数(上升沿和下降沿),计数方向由 C1IFP1 边沿方向和 C2IFP2 电平高低共同决定。
编码器模式 B: SMSEL=3’b010,计数器在 C2IFP2 边沿计数(上升沿和下降沿),计数方向由 C2IFP2 边沿方向和 C1IFP1 电平高低共同决定。
编码器模式 C: SMSEL=3’b011,计数器在 C1IFP1 和 C2IFP2 边沿计数(上升沿和下降沿),计数方向由 C1IFP1 边沿方向和 C2IFP2 电平高低、C2IFP2 边沿方向和 C1IFP1 电平高低共同决定共同决定。

主要用编码器模式 C,同时对两个通道的上升沿和下降沿都计数,那计数频率就变成了原始信号的 4 倍,即 4 倍频。这里计数举个例子,当通道 1 信号为高电平时,通道 2 信号为上升沿时,向上计数一次。
void timer_encoder_config(void)
{
gpio_init_type gpio_init_strcut;
crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);
crm_periph_clock_enable(CRM_TMR3_PERIPH_CLOCK, TRUE);
gpio_default_para_init(&gpio_init_strcut);
gpio_init_strcut.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
gpio_init_strcut.gpio_mode = GPIO_MODE_INPUT;
gpio_init_strcut.gpio_out_type = GPIO_OUTPUT_PUSH_PULL;
gpio_init_strcut.gpio_pins = GPIO_PINS_6 | GPIO_PINS_7;
gpio_init_strcut.gpio_pull = GPIO_PULL_NONE;
gpio_init(GPIOA, &gpio_init_strcut);
tmr_base_init(TMR3, 0xFFFF, 0);
tmr_cnt_dir_set(TMR3, TMR_COUNT_UP);
tmr_encoder_mode_config(TMR3, TMR_ENCODER_MODE_C, TMR_INPUT_RISING_EDGE, TMR_INPUT_RISING_EDGE);
tmr_counter_enable(TMR3, TRUE);
}

tmr_encoder_mode_config(TMR3, TMR_ENCODER_MODE_C, TMR_INPUT_RISING_EDGE, TMR_INPUT_RISING_EDGE);中的 TMR_INPUT_RISING_EDGE表示不反相,而在输入捕获时表示有效边沿为上升沿.。TMR_INPUT_FALLING_EDGE表示反相,也就说输入信号为上升沿时反相后为下降沿,这样硬件接反只需要更改下是否反相即可,同理在输入捕获时表示有效边沿为上升沿。TMR_INPUT_BOTH_EDGE表示不反相,在输入捕获中表示有效边沿为上升沿和下降沿。
PWM 输出
TMR 的输出部分由比较器和输出控制构成,用于编程输出信号的周期、占空比、极性。

配置 CxC[1:0]≠2'b00 将通道配置为输出可实现多种输出模式, 此时,计数器计数值将与通道寄存器 TMRx_CxDT)值比较,并根据 CxOCTRL[2:0]位配置的输出模式,产生中间信号 CxORAW(这边由PWM模式控制,PWM模式A和PWM模式B),再经过输出控制逻辑处理(选择OC极性来控制,也就是图中的Polarity Selection )后输送到 IO。输出信号的周期由周期寄存器(TMRx_PR)值配置,占空比则由通道寄存器(TMRx_CxDT)值配置。
PWM 模式 A:CxOCTRL=3'b110 时,开启 PWM 模式 A。 向上计数时,TMRx_C1DT>TMRx_CVAL 时 C1ORAW 输出高电平,否则为低电平;向下计数时,TMRx_C1DT<TMRx_CVAL 时 C1ORAW 输出低电平,否则为高电平。
PWM 模式 B:CxOCTRL=3'b111 时,开启 PWM 模式 B。 向上计数时,TMRx_C1DT>TMRx_CVAL 时 C1ORAW 输出低电平,否则为高电平;向下计数时,TMRx_C1DT<TMRx_CVAL 时 C1ORAW 输出高电平,否则为低电平。
这里STM32的参考手册写的最详细:PWM模式1- 在向上计数时,一旦TIMx_CNT<TIMx_CCR1时通道1为有效电平,否则为无效电平;在向下计数时,一旦TIMx_CNT>TIMx_CCR1时通道1为无效电平(OC1REF=0),否则为有效电平(OC1REF=1)。
这里的有效电平即为寄存器CCxP输入/捕获x输出极性,当CCx为0时,有效电平为高电平,为1时有效电平为低电平,一般有效电平都选高电平
/**
* @brief tmr output config type
*/
typedef struct
{
tmr_output_control_mode_type oc_mode; /* 比较输出模式 */
confirm_state oc_idle_state; /* 空闲状态下比较输出状态 */
confirm_state occ_idle_state; /* 空闲状态下比较互补输出状态 */
tmr_output_polarity_type oc_polarity; /* 输出极性 */
tmr_output_polarity_type occ_polarity; /* 互补输出极性 */
confirm_state oc_output_state; /* 比较输出使能 */
confirm_state occ_output_state; /* 比较互补输出使能 */
} tmr_output_config_type;
/**
* @brief 初始化PWM输出,并且开溢出中断
*
* @param tmr_x
*/
void timer_pwm_config(tmr_type *tmr_x)
{
gpio_init_type gpio_init_struct;
tmr_output_config_type tmr_output_config_struct;
crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);
crm_periph_clock_enable(CRM_TMR2_PERIPH_CLOCK, TRUE);
gpio_default_para_init(&gpio_init_struct);
gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
gpio_init_struct.gpio_mode = GPIO_MODE_MUX;
gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL;
gpio_init_struct.gpio_pins = GPIO_PINS_0 | GPIO_PINS_1 | GPIO_PINS_2;
gpio_init_struct.gpio_pull = GPIO_PULL_NONE;
gpio_init(GPIOA, &gpio_init_struct);
tmr_base_init(tmr_x, 999, 239);
tmr_cnt_dir_set(tmr_x, TMR_COUNT_UP);
tmr_output_default_para_init(&tmr_output_config_struct);
tmr_output_config_struct.oc_idle_state = FALSE;
tmr_output_config_struct.oc_mode = TMR_OUTPUT_CONTROL_PWM_MODE_B;
tmr_output_config_struct.oc_output_state = TRUE;
tmr_output_config_struct.oc_polarity = TMR_OUTPUT_ACTIVE_HIGH;
// tmr_output_config_struct.occ_idle_state = ;
// tmr_output_config_struct.occ_output_state = ;
// tmr_output_config_struct.occ_polarity = ;
tmr_output_channel_config(tmr_x, TMR_SELECT_CHANNEL_1, &tmr_output_config_struct);
tmr_channel_value_set(tmr_x, TMR_SELECT_CHANNEL_1, 0); //给比较值
tmr_output_channel_config(tmr_x, TMR_SELECT_CHANNEL_2, &tmr_output_config_struct);
tmr_channel_value_set(tmr_x, TMR_SELECT_CHANNEL_2, 0);
tmr_output_channel_config(tmr_x, TMR_SELECT_CHANNEL_3, &tmr_output_config_struct);
tmr_channel_value_set(tmr_x, TMR_SELECT_CHANNEL_3, 0);
// tmr_output_channel_config(tmr_x, TMR_SELECT_CHANNEL_4, &tmr_output_config_struct);
// tmr_channel_value_set(tmr_x, TMR_SELECT_CHANNEL_4, 0);
tmr_period_buffer_enable(tmr_x, TRUE); //开启周期缓冲使能,开启周期缓冲功能后(PRBEN 置 1),TMRx_PR寄存器值在溢出事件发生时传入它的影子寄存器
tmr_output_channel_buffer_enable(tmr_x, TMR_SELECT_CHANNEL_1, TRUE); // 启用TMRx_C1DT的缓存功能,写入TMRx_C1DT的内容将保存到缓存寄存器中,当发生溢出事件时再更新到TMRx_C1DT中。也就是写入比较值
tmr_output_channel_buffer_enable(tmr_x, TMR_SELECT_CHANNEL_2, TRUE);
tmr_output_channel_buffer_enable(tmr_x, TMR_SELECT_CHANNEL_3, TRUE);
nvic_priority_group_config(NVIC_PRIORITY_GROUP_2);
nvic_irq_enable(TMR2_GLOBAL_IRQn, 1, 1);
tmr_interrupt_enable(tmr_x, TMR_OVF_INT, TRUE);
// tmr_output_enable(tmr_x, TRUE);
tmr_counter_enable(tmr_x, TRUE);
}
/**
* @brief 简单的呼吸灯
*
*/
void TMR2_GLOBAL_IRQHandler(void)
{
static uint16_t s_pulse = 0;
static uint8_t s_direction = 0;
static uint16_t s_pulse_get = 0;
if (tmr_flag_get(TMR2, TMR_OVF_FLAG) != RESET)
{
tmr_flag_clear(TMR2, TMR_OVF_FLAG);
tmr_channel_value_set(TMR2, TMR_SELECT_CHANNEL_2, 999 - s_pulse);
tmr_channel_value_set(TMR2, TMR_SELECT_CHANNEL_3, s_pulse);
tmr_channel_value_set(TMR2, TMR_SELECT_CHANNEL_1, s_pulse);
s_pulse_get = tmr_channel_value_get(TMR2, TMR_SELECT_CHANNEL_1);
if (s_direction == 0)
{
s_pulse += 1;
}
else
{
s_pulse -= 1;
}
if (s_pulse >= 999)
{
s_direction = 1;
}
else if (s_pulse <= 20)
{
s_direction = 0;
}
SEGGER_RTT_printf(0, "s_pulse_get: %d\n", s_pulse_get);
}
}
输出比较
高级定时器
5.实时时钟(RTC)
实时时钟用于日历时钟功能,内部为一个 32 位递增计数器,通常使用中该计数器 1 秒增加 1,也就是该计数器相当于秒钟,然后根据当前的秒钟值,通过转换得到时间和日期,实现日历的功能,修改计数器的值便可修改时间和日期。RTC 计数逻辑位于电池供电域,只要 VBAT 有电, RTC 便会一直运行,不受系统复位以及 VDD 掉电影响。
时间戳
Unix 时间戳是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。
6.CRC 计算单元(CRC)
CRC介绍
循环冗余校验CRC(Cyclic Redundancy Check)是数据通信领域常用的一种数据传输检错技术。通过在发送端对数据按照某种算法计算出校验码,并将得到的校验码附在数据帧的后面,一起发送到接收端。接收端对收到的数据和校验码按照相同算法进行验证,以此判断接收到的数据是否正确、完整。
CRC基本概念
CRC 校验码位数 = 生成多项式位数 - 1。注意有些生成多项式的简记式中将生成多项式的最高位1省略了。
生成步骤:
- 将 X 的最高次幂为 R 的生成多项式 G(X)转换成对应的 R+1 位二进制数。
- 将信息码左移 R 位,相当于对应的信息多项式 C(X)*2R。
- 用生成多项式(二进制数)对信息码做除,得到 R 位的余数。
- 将余数拼到信息码左移后空出的位置,得到完整的 CRC 码。

上述推算过程,有助于我们理解 CRC 的概念。但直接编程来实现上面的算法,不仅繁琐,效率也不高。实际上在工程中不会直接这样去计算和验证 CRC。
编程实现
相对简单,先复位crc_data_reset();后面即可进行相关操作。
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*/
/* Private functions ---------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
#define BUFFER_SIZE 120
static const uint32_t data_buffer[BUFFER_SIZE] =
{
0xc33dd31c, 0xe73ff35e, 0x129022f3, 0x32d24235, 0x52146277, 0x7256b5ea,
0x4a755a54, 0x6a377a16, 0x0af11ad0, 0x2ab33a92, 0xed0fdd6c, 0xcd4dbdaa,
0xbb3bab1a, 0x6ca67c87, 0x5cc52c22, 0x3c030c60, 0x1c41edae, 0xfd8fcdec,
0xad8b9de8, 0x8dc97c26, 0x5c644c45, 0x3ca22c83, 0x1ce00cc1, 0xef1fff3e,
0x95a88589, 0xf56ee54f, 0xd52cc50d, 0x34e224c3, 0x04817466, 0x64475424,
0x78066827, 0x18c008e1, 0x28a3cb7d, 0xdb5ceb3f, 0xfb1e8bf9, 0x9bd8abbb,
0xdf7caf9b, 0xbfba8fd9, 0x9ff86e17, 0x7e364e55, 0x2e933eb2, 0x0ed11ef0,
0xa35ad3bd, 0xc39cf3ff, 0xe3de2462, 0x34430420, 0x64e674c7, 0x44a45485,
0xad2abd0b, 0x8d689d49, 0x7e976eb6, 0x5ed54ef4, 0x2e321e51, 0x0e70ff9f,
0xefbedfdd, 0xcffcbf1b, 0x9f598f78, 0x918881a9, 0xb1caa1eb, 0xd10cc12d,
0xe16f1080, 0x00a130c2, 0x20e35004, 0x40257046, 0x83b99398, 0xa3fbb3da,
0x00001021, 0x20423063, 0x408450a5, 0x60c670e7, 0x9129a14a, 0xb16bc18c,
0x569546b4, 0xb75ba77a, 0x97198738, 0xf7dfe7fe, 0xc7bc48c4, 0x58e56886,
0x4405a7db, 0xb7fa8799, 0xe75ff77e, 0xc71dd73c, 0x26d336f2, 0x069116b0,
0x76764615, 0x5634d94c, 0xc96df90e, 0xe92f99c8, 0xb98aa9ab, 0x58444865,
0x78a70840, 0x18612802, 0xc9ccd9ed, 0xe98ef9af, 0x89489969, 0xa90ab92b,
0xd1ade1ce, 0xf1ef1231, 0x32732252, 0x52b54294, 0x72f762d6, 0x93398318,
0xa56ab54b, 0x85289509, 0xf5cfc5ac, 0xd58d3653, 0x26721611, 0x063076d7,
0x8d689d49, 0xf7dfe7fe, 0xe98ef9af, 0x063076d7, 0x93398318, 0xb98aa9ab,
0x4ad47ab7, 0x6a961a71, 0x0a503a33, 0x2a12dbfd, 0xfbbfeb9e, 0x9b798b58};
__IO uint32_t crc_value = 0;
/**
* @brief main function.
* @param none
* @retval none
*/
int main(void)
{
system_clock_config();
mx_rtc_clock_init();
crm_periph_clock_enable(CRM_CRC_PERIPH_CLOCK, TRUE);
crc_data_reset();
crc_value = crc_block_calculate((uint32_t *)data_buffer, BUFFER_SIZE);
SEGGER_RTT_printf(0, "CRC value: 0x%x\n", crc_value);
while (1)
{
}
}
↩︎/** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { LL_FLASH_SetLatency(LL_FLASH_LATENCY_3); while (LL_FLASH_GetLatency() != LL_FLASH_LATENCY_3) { } LL_PWR_SetRegulVoltageScaling(LL_PWR_REGU_VOLTAGE_SCALE1); LL_RCC_HSE_Enable(); /* Wait till HSE is ready */ while (LL_RCC_HSE_IsReady() != 1) { } LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE, LL_RCC_PLLM_DIV_12, 96, LL_RCC_PLLP_DIV_2); LL_RCC_PLL_Enable(); /* Wait till PLL is ready */ while (LL_RCC_PLL_IsReady() != 1) { } LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1); LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_2); LL_RCC_SetAPB2Prescaler(LL_RCC_APB2_DIV_1); LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL); /* Wait till System clock is ready */ while (LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL) { } LL_Init1msTick(100000000); LL_SetSystemCoreClock(100000000); LL_RCC_SetTIMPrescaler(LL_RCC_TIM_PRESCALER_TWICE); }- 先声明GPIO初始化的结构体
- 打开GPIOx的时钟
- 设置初始电平
- 配置结构体
- 把结构体装入初始化函数
浙公网安备 33010602011771号