秋招复习笔记——嵌入式裸机开发

news2024/11/24 1:47:10

底层相关的内容,之前掌握的不扎实,现在重新把相关重点记录一下,做个笔记记诵。

相关基础知识

ST简单内容

用的F103ZET6,72MHz,FLASH是512KB,SRAM是64KB,144个引脚,2基本定时器,4通用计时器,2高级计时器,ADC是3路,通道18个,IIC是2个,SPI是3个,USART是5个,CAN有1个,DMA有2路,这是大致用过的一些资源。

用的F407IGT6,168MHz,FLASH是1024KB,SRAM是192KB,176个引脚,2个基本定时器,10通用计时器,2高级计时器,ADC是3路,通道18个,IIC是3个,SPI是3个,USART是6个,CAN有2个,DMA有2路,这是大致用过的一些资源。

用的F407VET6,168MHz,FLASH是512KB,SRAM是192KB,100个引脚,2个基本定时器,10通用计时器,2高级计时器,ADC是3路,通道18个,IIC是3个,SPI是3个,USART是6个,CAN有2个,DMA有2路,这是大致用过的一些资源。

启动过程设置

可以通过BOOT0和BOOT1选择:BOOT0=0,就是FLASH启动,跳转到0x08000000;BOOT0=1,BOOT1=0,系统存储器启动,用于串口下载,跳转到0x1FFFF000;BOOT0=1,BOOT1=1,就是SRAM启动,比较少用,跳转到0x20000000。

电机开发板是没得选的,精英板可以选。

首先:两个BOOT都需要接到GND,然后,B0接3V3,B1还是GND,按一下复位,然后就可以下载代码了。现在进行电路设计,串口的DTR和RTS信号控制复位和B0,DTR低电平复位,RTS高电平进BootLoader

M3内核

M3 内核

驱动单元 M3 的 DCode 接到内部 FLASH;System 接到了内部 SRAM;DMA1 去到 FSMC;DMA2 去到所有的 APB 设备。

  1. IBus 指令总线,连到 FLASH,获取指令;
  2. DBus,数据总线,连到 SRAM、FLASH 等,访问数据;
  3. SBus,连接到所有外设;
  4. DMA 总线,实现数据自动搬运;
  5. 内部 FLASH,硬盘,代码/数据存储,最高 24MHz,所以因为 72MHz的主频率,插入 2 个时钟周期延迟;
  6. 内部 SRAM,内存,数据存储,最快 72MHz;
  7. FSMC,灵活静态存储控制器,就是外部总线接口,可以访问 SRAM、NAND FLASH、NOR FLASH 等;
  8. AHB/APB 桥,AHB 最高 72MHz,APB2 一样,但是 APB1 最高是 36MHz;
  9. 总线矩阵,访问仲裁用。

存储器需要映射,ST 是将 4GB(32位芯片)分为 8 个块,如下:

地址映射

主要就是前三个块,直接截图稍微记一记:

Block 0

Block 1

Block 2

如果要直接操作寄存器,那就去找地址,寄存器地址 = BUS_BASE_ADDR + PERIPH_OFFSET + REG_OFFSET(总线基地址 + 外设基于总线基地址的偏移量 + 寄存器相对外设基地址的偏移量)。寄存器映射都在 stm32f103xw.h 中。

M4 内核

一样,贴一个电机开发板的:

F407系统架构
是类似的,这里写一点区别:

  1. 内部 FLASH,168MHz的主频,所以要插入 8 个时钟周期延迟;
  2. 内部 SRAM,可以实现最高 168MHz;
  3. AHB 和 APB2 最高 84MHz,APB1 只有 42MHz。

其余都是差不多的。

启动过程详解

三种复位:上电复位、硬件复位、软件复位。复位之后两件事:

  1. 从地址 0x0000 0000 处取出堆栈指针 MSP 的初始值,该值就是栈顶地址。
  2. 从地址 0x0000 0004 处取出 程序计数器指针 PC 的初始值,该值指向复位后执行的第一条指令。

三种情况:

  • FLASH 启动:映射到 0x08000000 和 0x08000004,第一个存的是栈指针 MSP,然后是程序指针 PC,之后就可以从 PC 读取指令;
  • SRAM 启动:映射到 0x20000000 和 0x20000004;实际通过 startup_stm32f103xw.s 决定,链接通过分散加载文件(sct)决定绝对地址,是分配到FLASH 还是 SRAM;
  • 系统存储器:系统存储器的 0x1FFFF000 及 0x1FFFF004 获取 MSP 及 PC值进行自举。这个是用户无法访问的,但可以 ISP,根据 USART1 的信息来更新自己的内部 FLASH 来升级。

启动做的事情:

  1. 初始化堆栈指针 SP = _initial_sp
  2. 初始化程序计数器指针 PC = Reset_Handler
  3. 设置堆和栈的大小
  4. 初始化中断向量表
  5. 配置外部 SRAM作为数据存储器(可选)
  6. 配置系统时钟,通过调用 SystemInit函数(可选)
  7. 调用 C库中的 _main 函数初始化用户堆栈,最终调用 main 函数

编译完,会有很多文件,map 文件就可以看交叉链接信息。有五个部分:

  • 程序段交叉引用关系(Section Cross References)
  • 删除映像未使用的程序段(Removing Unused input sections from the image)
  • 映像符号表(Image Symbol Table)
  • 映像内存分布图(Memory Map of the image)
  • 映像组件大小(Image component sizes)

内容大致有这些:

  • Section:描述映像文件的代码或数据块,我们简称程序段
  • RO:Read Only 的缩写,包括只读数据(RO data)和代码(RO code)两部分内容,占用 FLASH 空间
  • RW:Read Write 的缩写,包含可读写数据(RW data,有初值,且不为 0),占用 FLASH(存储初值)和 RAM(读写操作)
  • ZI:Zero initialized 的缩写,包含初始化为 0 的数据(ZI data),占用 RAM 空间。
  • .text:相当于 RO code
  • .constdata:相当于 RO data
  • .bss:相当于 ZI data
  • .data:相当于 RW data

时钟树

F103 的话,就是 8MHz的 HSE,32.768kHz 的 LSE,HSI是 8MHz,LSI是 40kHz。

LSE 是 RTC 的时钟源,LSI 是独立看门狗的时钟源。

最后是 8MHz 的 HSE,HSE 不分频,然后锁相环 PLL 9 倍频,AHB 不分频,APB1 2分频,但是 APB1 会时钟倍频,APB2 不分频

F407 类似,就是 8MHz的 HSE,32.768kHz 的 LSE,HSI 是 16MHz,LSI是32kHz。

最后是 8MHz 的 HSE,HSE 8 分频,然后锁相环 PLL 168 倍频,AHB 不分频,APB1 4分频,但是 APB1 会时钟倍频,APB2 2 分频,时钟也会倍频

HSE_VALUE 这个宏定义,需要手动写成 800000U;F103 直接 PLLMUL给到 9就可以了,F407 需要配置 3 个,分别到 PLLN 336,PLLM 8,PLLP 2 以及 PLLQ 7。APB1 和 APB2 的分频也搞定之后,F103 的 HAL_RCC_ClockConfig 给到 2WS,F407 就要给到 5WS 了(这个就是之前的时间延迟)。

SYSTEM 相关

delay 需要操作的配置,F103 配了 8 分频是因为后面的 delay_us 是直接读取的 SysTick->CTRL,24位递减的话不分频不够;F407 不分频是因为算好了节拍之后用的SysTick->LOAD 以及 VAL 来完成的计算。(其实带了 OS 之后都是一样的,所以也可以不分频)。

还有就是需要重定位 printf 函数来完成打印。因为直接用,单片机会进入搬主机模式,必须用仿真器调试,没办法完成显示。所以,需要 printf 中的 fputc 重新实现完成重定向,还得避免进入半主机模式。

方法:fputc写一下,把 usart 的 SR 寄存器&0x40==0 放在 while,然后发送就是把 参数 ch 的内容写到 DR 寄存器。Keil 要加 #param还有 FILE ttywrch sys_exit等。

相关用到的外设驱动

GPIO

最经典的,要记住的是 8 种功能模式。具体的电路图不记了,应该不会那么细致。

  1. 输入浮空:上下拉断开,施密特触发器打开,输出禁止。IO 电平完全由外部电路决定,用于按键检测等
  2. 输入上拉:上拉电阻导通其余一致。
  3. 输入下拉:下拉电阻导通其余一致。
  4. 模拟功能:上下拉断开,施密特触发器关闭,双 MOS 关闭**。用于 ADC、DAC等,也有休眠省电的配置。**
  5. 开漏输出:只能输出低电平 Vss 或者高阻态,P-MOS 一直截止,不导通,相当于一直 VDD,常用于 IIC(IIC_SDA)等。开漏输出模式下,可以读取 IO 引脚状态
  6. 推挽输出:输出低电平 Vss 或者高电平 VDD。推挽输出跟开漏输出不同的是,推挽输出模式 P-MOS 管和 N-MOS 管都用上。同样可以读取 IO 电平。
  7. 开漏复用:IO 作为其他外设的特殊功能引脚。状态由相应外设控制,而不是输出寄存器。其余就是开漏输出。
  8. 推挽复用:复用和推挽两个结合。

像使用 OLED 的时候,用了软件模拟 IIC,就要读取 IIC_SDA 引脚电平,直接读很慢,可以修改 GPIO->MODER ,来切换输入输出加快速度。(输入就是 22 位配0,输出22位配1)

配置的时候,需要的是 GPIO_InitTypeDef 的结构体成员完成,设置 Pin,Mode,Pull 和 Speed(F4 多一个复用功能的Alternate);最后记得 HAL_GPIO_Init 初始化。

HAL_GPIO_WritePin,HAL_GPIO_ReadPin 两个函数,还有取反的 HAL_GPIO_TogglePin。可以用 do while(0)来保证操作是原子操作。

按键输入

这个就是 GPIO 输入的例子。

配置还是上一章的结构体,就是配置为输入模式,如果低电平有效那就上拉,反之下拉。

外部中断

F103,一共有系统中断 10 个,外部中断 60 个;F407 是系统中断10 个,外部中断 82 个。

NVIC 相关内容,通过调用 HAL_NVIC_SetPriorityGrouping 来完成抢占优先级的设置,一般裸机开发给到的是 NVIC_PRIORITYGROUP_2,FreeRTOS 就是直接给到 NVIC_PRIORITYGROUP_5。

EXTI 就是外部中断的控制器,一共两条主线,一个是输入线到 NVIC 中断控制器,一个是输入线到脉冲发生器。支持 19 个外部中断,都是输入线的:0~15 对应外部 IO 口中断,16 对应 PVD 输出,17 对应 RTC 闹钟,18 对应 USB 唤醒,19 对应以太网唤醒。也就是说,IO 对应的中断只有 16 个,所有的GPIOx.0 都会对应到 EXTI0,配置来完成具体的对应关系。(如果是F4,再多 3 根线,20 对应USB OTG HS唤醒,21 对应 RTC 入侵,22 对应 RTC 唤醒)。

要注意的是,在初始化函数中,需要配置 HAL_NVIC_SetPriority 以及 HAL_NVIC_EnableIRQ() 完成优先级配置以及使能。配置的时候,还要注意,GPIO 模式需要选择外部中断的触发方式,GPIO_MODE_IT_FALLING、GPIO_MODE_IT_RISING 以及 GPIO_MODE_IT_RISING_FALLING。

外部中断函数是 7 个,EXTIx_IRQHandler(),x 取值 0~4,然后是 EXTI9_5_IRQ_Handler() 以及 EXTI15_10_IRQ_Handler(),是 5-9共用一个,10-15 共用一个,以及对应的EXTIx_IRQn这个中断服务函数名称,配置的时候就是通过这个完成优先级等的配置。对应的中断回调是 HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)。

串口

F1 中,是 3 个 USART 以及 2 个 UART。(F4 多 1 个 USART)。

USART1 是 APB2 总线,72MHz,剩余的都是 APB1 的 36MHz;F4 中 USART1 以及 USART6 是 APB2 的 84MHz,其余的是 APB1 的 42MHz。

一般用的就是 UART,异步全双工数据通信。配置的话,**一般都是 0 作为起始位,停止位用 1 个 1,不设置校验位。**数据实际的收发,是到了 USART_DR 寄存器中的 TDR 和 RDR(收发是分开的),只有一个字节长度;状态在 USART_SR 中,主要是 RXNE 和 TC,对应了 RXNE 是 1 就收到了,TC 为 1 就是发完了。

这里第一次涉及引脚复用。F1中,需要去调用 __HAL_AFIO_REMAP_USART1_ENABLE(),但是因为用的是默认的 PA9 和 PA10,所以不需要复用(PB6 和 PB7需要);F4中,则是直接 GPIO_AF7_USART1

初始化的串口句柄是 UART_HandleTypeDef 结构体;配置的结构体是在这个句柄中包含的 UART_InitTypeDef,完成了波特率,通讯方式等的配置。

具体的配置,我直接记录:

F1 的在这里:

UART_HandleTypeDef g_uart1_handle;  /* UART句柄 */
void usart_init(uint32_t baudrate)
{
    /*UART 初始化设置*/
    g_uart1_handle.Instance = USART_UX;                                       /* USART_UX */
    g_uart1_handle.Init.BaudRate = baudrate;                                  /* 波特率 */
    g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;                      /* 字长为8位数据格式 */
    g_uart1_handle.Init.StopBits = UART_STOPBITS_1;                           /* 一个停止位 */
    g_uart1_handle.Init.Parity = UART_PARITY_NONE;                            /* 无奇偶校验位 */
    g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;                      /* 无硬件流控 */
    g_uart1_handle.Init.Mode = UART_MODE_TX_RX;                               /* 收发模式 */
    HAL_UART_Init(&g_uart1_handle);                                           /* HAL_UART_Init()会使能UART1 */

    /* 该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量 */
    HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE); 
}

在 MspInit 之中,就是配置一下对应的 GPIO,复用推挽输出上拉高速(F4还要配置一下复用的 Alternate 复用成 USART1)。

基本定时器

基本定时器,基本就只能做一点周期性的操作,使用更新中断来完成。

配置的时候是 TIM_HandleTypeDef 结构体,指定了引脚之后,就是里面的 TIM_Base_InitTypeDef 结构体配置,完成一系列初始化。

具体的直接看:

TIM_HandleTypeDef g_timx_handle;  /* 定时器句柄 */
/**
 * @brief       基本定时器TIMX定时中断初始化函数
 * @note
 *              基本定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
 *              基本定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
 *              Ft=定时器工作频率,单位:Mhz
 *
 * @param       arr: 自动重装值。
 * @param       psc: 时钟预分频数
 * @retval      无
 */
void btim_timx_int_init(uint16_t arr, uint16_t psc)
{
    g_timx_handle.Instance = BTIM_TIMX_INT;                      /* 通用定时器X */
    g_timx_handle.Init.Prescaler = psc;                          /* 设置预分频系数 */
    g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP;         /* 递增计数模式 */
    g_timx_handle.Init.Period = arr;                             /* 自动装载值 */
    HAL_TIM_Base_Init(&g_timx_handle);

    HAL_TIM_Base_Start_IT(&g_timx_handle);    /* 使能定时器x及其更新中断 */
}

这里主要就是记住,配置的 psc 和 arr 会最终影响定时器的频率。

F1 中,TIM2-7 都是 APB1 的时间总线,因为还倍频了所以就是 72MHz;F4 中,TIM2-7 都是 APB1 的时间总线,因为还倍频了所以就是 84MHz。

这里*更新中断回调函数,是需要自己来写的,对应的是 HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef htim)。

通用定时器

这里主要还涉及一个从模式的控制寄存器,一般都是不用的。开启的话就可以实现定时器的级联,可以时基对齐。

主要的区别在于,可以输出 PWM,可以完成输入捕获和脉冲计数,下面一个个看。

对于PWM 输出,一共有四种情况:

PWM 的四种情况

这个还是要记一下,关键的八股文。PWM1,那就是超过了 CCRx 的时候输出低电平;PWM2 反过来就可以了。通用定时器可以产生 4 路 PWM 波,但是频率是一样的,占空比彼此独立可调。

TIM_HandleTypeDef 结构体是一样的,但是还要设置 TIM_OC_InitTypeDef 结构体类型,其中配置 PWM 的相关参数。

相关配置如下:

TIM_HandleTypeDef g_timx_pwm_chy_handle;     /* 定时器x句柄 */

/**
 * @brief       通用定时器TIMX 通道Y PWM输出 初始化函数(使用PWM模式1)
 * @note
 *              通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
 *              通用定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
 *              Ft=定时器工作频率,单位:Mhz
 *
 * @param       arr: 自动重装值。
 * @param       psc: 时钟预分频数
 * @retval      无
 */
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_pwm_chy  = {0};                          /* 定时器PWM输出配置 */

    g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM;                     /* 定时器x */
    g_timx_pwm_chy_handle.Init.Prescaler = psc;                         /* 定时器分频 */
    g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;        /* 递增计数模式 */
    g_timx_pwm_chy_handle.Init.Period = arr;                            /* 自动重装载值 */
    HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);                           /* 初始化PWM */

    timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;                           /* 模式选择PWM1 */
    timx_oc_pwm_chy.Pulse = arr / 2;                                    /* 设置比较值,此值用来确定占空比 */
                                                                        /* 默认比较值为自动重装载值的一半,即占空比为50% */
    timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_LOW;                    /* 输出比较极性为低 */
    HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &timx_oc_pwm_chy, GTIM_TIMX_PWM_CHY); /* 配置TIMx通道y */
    HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);       /* 开启对应PWM通道 */
}

/**
 * @brief       定时器底层驱动,时钟使能,引脚配置
                此函数会被HAL_TIM_PWM_Init()调用
 * @param       htim:定时器句柄
 * @retval      无
 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == GTIM_TIMX_PWM)
    {
        GPIO_InitTypeDef gpio_init_struct;
        GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE();               /* 开启通道y的CPIO时钟 */
        GTIM_TIMX_PWM_CHY_CLK_ENABLE();

        gpio_init_struct.Pin = GTIM_TIMX_PWM_CHY_GPIO_PIN; /* 通道y的CPIO口 */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;           /* 复用推完输出 */
        gpio_init_struct.Pull = GPIO_PULLUP;               /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;     /* 高速 */
        HAL_GPIO_Init(GTIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);
        GTIM_TIMX_PWM_CHY_GPIO_REMAP();                    /* IO口REMAP设置, 是否必要查看头文件配置的说明 */
    }
}

F4 的话,就是把最后的 REMAP,换成在结构体中的 Alternate 复用。调整占空比,可以用 __HAL_TIM_SET_COMPARE 来完成,也可以直接用 CCR 寄存器调整,是一样的。注意的是,配置完了之后,需要 HAL_TIM_PWM_ConfigChannel 配置好 PWM 输出的参数使能。

这里同时注意 OCPolarity 这个参数,设置为 LOW 那就是输出比较极性为低,低电平有效,也就是设置的 CCR 的值,如果 cnt < CCR,那么这个时候电平是低的,cnt > CCR 输出高电平;所以占空比就是 (CCR - CNT) / CCR

输入捕获,同样需要 TIM_HandleTypeDef 结构体,之后还需要 TIM_IC_InitTypeDef 结构体,完成对输入捕获的相关配置。同样的,还需要些更新中断回调函数 HAL_TIM_PeriodElapsedCallback(),以及捕获中断回调函数 HAL_TIM_IC_CaptureCallback()。

代码如下,主要还是关注基础配置:

TIM_HandleTypeDef g_timx_cap_chy_handle;      /* 定时器x句柄 */

/**
 * @brief       通用定时器TIMX 通道Y 输入捕获 初始化函数
 * @note
 *              通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
 *              通用定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
 *              Ft=定时器工作频率,单位:Mhz
 *
 * @param       arr: 自动重装值
 * @param       psc: 时钟预分频数
 * @retval      无
 */
void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_IC_InitTypeDef timx_ic_cap_chy = {0};

    g_timx_cap_chy_handle.Instance = GTIM_TIMX_CAP;                     /* 定时器5 */
    g_timx_cap_chy_handle.Init.Prescaler = psc;                         /* 定时器分频 */
    g_timx_cap_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;        /* 递增计数模式 */
    g_timx_cap_chy_handle.Init.Period = arr;                            /* 自动重装载值 */
    HAL_TIM_IC_Init(&g_timx_cap_chy_handle);

    timx_ic_cap_chy.ICPolarity = TIM_ICPOLARITY_RISING;                 /* 上升沿捕获 */
    timx_ic_cap_chy.ICSelection = TIM_ICSELECTION_DIRECTTI;             /* 映射到TI1上 */
    timx_ic_cap_chy.ICPrescaler = TIM_ICPSC_DIV1;                       /* 配置输入分频,不分频 */
    timx_ic_cap_chy.ICFilter = 0;                                       /* 配置输入滤波器,不滤波 */
    HAL_TIM_IC_ConfigChannel(&g_timx_cap_chy_handle, &timx_ic_cap_chy, GTIM_TIMX_CAP_CHY);  /* 配置TIM5通道1 */

    __HAL_TIM_ENABLE_IT(&g_timx_cap_chy_handle, TIM_IT_UPDATE);         /* 使能更新中断 */
    HAL_TIM_IC_Start_IT(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);     /* 开始捕获TIM5的通道1 */
}

/**
 * @brief       通用定时器输入捕获初始化接口
                HAL库调用的接口,用于配置不同的输入捕获
 * @param       htim:定时器句柄
 * @note        此函数会被HAL_TIM_IC_Init()调用
 * @retval      无
 */
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == GTIM_TIMX_CAP)                    /*输入通道捕获*/
    {
        GPIO_InitTypeDef gpio_init_struct;
        GTIM_TIMX_CAP_CHY_CLK_ENABLE();                     /* 使能TIMx时钟 */
        GTIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE();                /* 开启捕获IO的时钟 */

        gpio_init_struct.Pin = GTIM_TIMX_CAP_CHY_GPIO_PIN;  /* 输入捕获的GPIO口 */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;            /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_PULLDOWN;              /* 下拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */
        HAL_GPIO_Init(GTIM_TIMX_CAP_CHY_GPIO_PORT, &gpio_init_struct);

        HAL_NVIC_SetPriority(GTIM_TIMX_CAP_IRQn, 1, 3);     /* 抢占1,子优先级3 */
        HAL_NVIC_EnableIRQ(GTIM_TIMX_CAP_IRQn);             /* 开启ITMx中断 */
    }
}

/* 输入捕获状态(g_timxchy_cap_sta)
 * [7]  :0,没有成功的捕获;1,成功捕获到一次.
 * [6]  :0,还没捕获到高电平;1,已经捕获到高电平了.
 * [5:0]:捕获高电平后溢出的次数,最多溢出63次,所以最长捕获值 = 63*65536 + 65535 = 4194303
 *       注意:为了通用,我们默认ARR和CCRy都是16位寄存器,对于32位的定时器(如:TIM5),也只按16位使用
 *       按1us的计数频率,最长溢出时间为:4194303 us, 约4.19秒
 *
 *      (说明一下:正常32位定时器来说,1us计数器加1,溢出时间:4294秒)
 */
uint8_t g_timxchy_cap_sta = 0;    /* 输入捕获状态 */
uint16_t g_timxchy_cap_val = 0;   /* 输入捕获值 */


/**
 * @brief       定时器中断服务函数
 * @param       无
 * @retval      无
 */
void GTIM_TIMX_CAP_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&g_timx_cap_chy_handle);  /* 定时器HAL库共用处理函数 */
}

/**
 * @brief       定时器输入捕获中断处理回调函数
 * @param       htim:定时器句柄指针
 * @note        该函数在HAL_TIM_IRQHandler中会被调用
 * @retval      无
 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == GTIM_TIMX_CAP)
    {
        if ((g_timxchy_cap_sta & 0X80) == 0)                /* 还未成功捕获 */
        {
            if (g_timxchy_cap_sta & 0X40)                   /* 捕获到一个下降沿 */
            {
                g_timxchy_cap_sta |= 0X80;                  /* 标记成功捕获到一次高电平脉宽 */
                g_timxchy_cap_val = HAL_TIM_ReadCapturedValue(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);  /* 获取当前的捕获值 */
                TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);                      /* 一定要先清除原来的设置 */
                TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_RISING); /* 配置TIM5通道1上升沿捕获 */
            }
            else /* 还未开始,第一次捕获上升沿 */
            {
                g_timxchy_cap_sta = 0;                              /* 清空 */
                g_timxchy_cap_val = 0;
                g_timxchy_cap_sta |= 0X40;                          /* 标记捕获到了上升沿 */
                __HAL_TIM_DISABLE(&g_timx_cap_chy_handle);          /* 关闭定时器5 */
                __HAL_TIM_SET_COUNTER(&g_timx_cap_chy_handle, 0);   /* 定时器5计数器清零 */
                TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);   /* 一定要先清除原来的设置!! */
                TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_FALLING); /* 定时器5通道1设置为下降沿捕获 */
                __HAL_TIM_ENABLE(&g_timx_cap_chy_handle);           /* 使能定时器5 */
            }
        }
    }
}

/**
 * @brief       定时器更新中断回调函数
 * @param        htim:定时器句柄指针
 * @note        此函数会被定时器中断函数共同调用的
 * @retval      无
 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == GTIM_TIMX_CAP)
    {
        if ((g_timxchy_cap_sta & 0X80) == 0)            /* 还未成功捕获 */
        {
            if (g_timxchy_cap_sta & 0X40)               /* 已经捕获到高电平了 */
            {
                if ((g_timxchy_cap_sta & 0X3F) == 0X3F) /* 高电平太长了 */
                {
                    TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);                     /* 一定要先清除原来的设置 */
                    TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_RISING);/* 配置TIM5通道1上升沿捕获 */
                    g_timxchy_cap_sta |= 0X80;          /* 标记成功捕获了一次 */
                    g_timxchy_cap_val = 0XFFFF;
                }
                else      /* 累计定时器溢出次数 */
                {
                    g_timxchy_cap_sta++;
                }
            }
        }
    }
}

这里就相当于是外部时钟模式,会把外部时钟源信号,借由 IO 读到对应的定时器通道,而且只能走 CH1 或者 CH2。

注意在 init 函数中,配置好了对应的参数之后,需要 HAL_TIM_IC_ConfigChannel 来完成配置。其余的就是自己设计的读取高电平的一些状态位,看一下就好了。

最后还有一个定时器脉冲计数的实验,这个要关注一下,因为涉及到了主从模式。同样需要 TIM_HandleTypeDef 结构体,以及配置从模式的 TIM_SlaveConfigTypeDef 结构体。

最终代码如下:

TIM_HandleTypeDef g_timx_cnt_chy_handle;        /* 定时器x句柄 */

/* 记录定时器计数器的溢出次数, 方便计算总脉冲个数 */
uint32_t g_timxchy_cnt_ofcnt = 0 ;              /* 计数溢出次数 */

/**
 * @brief       通用定时器TIMX 通道Y 脉冲计数 初始化函数
 * @note
 *              本函数选择通用定时器的时钟选择: 外部时钟源模式1(SMS[2:0] = 111)
 *              这样CNT的计数时钟源就来自 TIMX_CH1/CH2, 可以实现外部脉冲计数(脉冲接入CH1/CH2)
 *
 *              时钟分频数 = psc, 一般设置为0, 表示每一个时钟都会计数一次, 以提高精度.
 *              通过读取CNT和溢出次数, 经过简单计算, 可以得到当前的计数值, 从而实现脉冲计数
 *
 * @param       arr: 自动重装值 
 * @retval      无
 */
void gtim_timx_cnt_chy_init(uint16_t psc)
{
    GPIO_InitTypeDef gpio_init_struct;
    TIM_SlaveConfigTypeDef tim_slave_config = {0};
    GTIM_TIMX_CNT_CHY_CLK_ENABLE();                                          /* 使能TIMx时钟 */
    GTIM_TIMX_CNT_CHY_GPIO_CLK_ENABLE();                                     /* 开启GPIOA时钟 */
    
    g_timx_cnt_chy_handle.Instance = GTIM_TIMX_CNT;                          /* 定时器x */
    g_timx_cnt_chy_handle.Init.Prescaler = psc;                              /* 定时器分频 */
    g_timx_cnt_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;             /* 递增计数模式 */
    g_timx_cnt_chy_handle.Init.Period = 65535;                               /* 自动重装载值 */
    HAL_TIM_IC_Init(&g_timx_cnt_chy_handle);

    gpio_init_struct.Pin = GTIM_TIMX_CNT_CHY_GPIO_PIN;                       /* 输入捕获的GPIO口 */
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;                                 /* 复用推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLDOWN;                                   /* 下拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;                           /* 高速 */
    HAL_GPIO_Init(GTIM_TIMX_CNT_CHY_GPIO_PORT, &gpio_init_struct);

    /* 从模式:外部触发模式1 */
    tim_slave_config.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;                    /* 从模式:外部触发模式1 */
    tim_slave_config.InputTrigger = TIM_TS_TI1FP1;                           /* 输入触发:选择 TI1FP1(TIMX_CH1) 作为输入源 */
    tim_slave_config.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;           /* 触发极性:上升沿 */
    tim_slave_config.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1;           /* 触发预分频:无 */
    tim_slave_config.TriggerFilter = 0x0;                                    /* 滤波:本例中不需要任何滤波 */
    HAL_TIM_SlaveConfigSynchro(&g_timx_cnt_chy_handle, &tim_slave_config);

    HAL_NVIC_SetPriority(GTIM_TIMX_CNT_IRQn, 1, 3);                          /* 设置中断优先级,抢占优先级1,子优先级3 */
    HAL_NVIC_EnableIRQ(GTIM_TIMX_CNT_IRQn);

    __HAL_TIM_ENABLE_IT(&g_timx_cnt_chy_handle, TIM_IT_UPDATE);              /* 使能更新中断 */
    HAL_TIM_IC_Start(&g_timx_cnt_chy_handle, GTIM_TIMX_CNT_CHY);             /* 开始捕获TIMx的通道y */
}

/**
 * @brief       通用定时器TIMX 通道Y 获取当前计数值 
 * @param       无
 * @retval      当前计数值
 */
uint32_t gtim_timx_cnt_chy_get_count(void)
{
    uint32_t count = 0;
    count = g_timxchy_cnt_ofcnt * 65536;                     /* 计算溢出次数对应的计数值 */
    count += __HAL_TIM_GET_COUNTER(&g_timx_cnt_chy_handle);  /* 加上当前CNT的值 */
    return count;
}

/**
 * @brief       通用定时器TIMX 通道Y 重启计数器
 * @param       无
 * @retval      当前计数值
 */
void gtim_timx_cnt_chy_restart(void)
{
    __HAL_TIM_DISABLE(&g_timx_cnt_chy_handle);        /* 关闭定时器TIMX */
    g_timxchy_cnt_ofcnt = 0;                          /* 累加器清零 */
    __HAL_TIM_SET_COUNTER(&g_timx_cnt_chy_handle, 0); /* 计数器清零 */
    __HAL_TIM_ENABLE(&g_timx_cnt_chy_handle);         /* 使能定时器TIMX */
}

/**
 * @brief       通用定时器TIMX 脉冲计数 更新中断服务函数
 * @param       无
 * @retval      无
 */
void GTIM_TIMX_CNT_IRQHandler(void)
{
    /* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式 */
    if(__HAL_TIM_GET_FLAG(&g_timx_cnt_chy_handle, TIM_FLAG_UPDATE) != RESET)
    {
        g_timxchy_cnt_ofcnt++;          /* 累计溢出次数 */
    }

      __HAL_TIM_CLEAR_IT(&g_timx_cnt_chy_handle, TIM_IT_UPDATE);
}

没有中断回调函数,直接在 IRQHandler 函数中,通过状态位的判断来完成溢出次数统计以及对应的状态位清除。这里是配置成了外部时钟模式1,定时器输入 1 的 TI1FP1,不滤波也不分频。最后 HAL_TIM_SlaveConfigSynchro 配置完从模式。

高级定时器

再次多出几个功能,有了重复计数器,可以控制重复寄存器达到了写入的值才发生更新溢出;输出比较,可以有带死区的互补输出功能;断路功能,该工嗯呢可以使得输出不能同时有效。

PWM 输出,对于寄存器层面而言,高级定时器需要 MOE 位置 1 才能正常输出

如果要输出指定个数的 PWM,PWM 相关的配置其实是类似的,高级定时器就是多了 AutoReloadPreload,控制影子寄存器缓冲是否打开,就可以让 arr 的值更加稳定;还有 RepetitionCounter 设置重复计数器。为了统计个数,就需要 HAL_TIM_GenerateEvent 产生更新事件进中断,进中断函数就可以用 TIM_FLAG_UPDATE 来判断是否为 RESET,不是那就可以进中断,根据全局变量配置 RCR 寄存器然后一样产生更新中断就可以了RCR 就是重复计数器寄存器,其存储的值就是 PWM 的个数

代码如下:

TIM_HandleTypeDef g_timx_npwm_chy_handle;     /* 定时器x句柄 */

/* g_npwm_remain表示当前还剩下多少个脉冲要发送
 * 每次最多发送256个脉冲
 */
static uint32_t g_npwm_remain = 0;

/**
 * @brief       高级定时器TIMX 通道Y 输出指定个数PWM 初始化函数
 * @note
 *              高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
 *              高级定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
 *              Ft=定时器工作频率,单位:Mhz
 *
 * @param       arr: 自动重装值
 * @param       psc: 时钟预分频数
 * @retval      无
 */
void atim_timx_npwm_chy_init(uint16_t arr, uint16_t psc)
{
    GPIO_InitTypeDef gpio_init_struct;
    TIM_OC_InitTypeDef timx_oc_npwm_chy;   /* 定时器输出 */
    ATIM_TIMX_NPWM_CHY_GPIO_CLK_ENABLE();  /* TIMX 通道IO口时钟使能 */
    ATIM_TIMX_NPWM_CHY_CLK_ENABLE();       /* TIMX 时钟使能 */

    g_timx_npwm_chy_handle.Instance = ATIM_TIMX_NPWM;                  /* 定时器x */
    g_timx_npwm_chy_handle.Init.Prescaler = psc;                       /* 定时器分频 */
    g_timx_npwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;      /* 递增计数模式 */
    g_timx_npwm_chy_handle.Init.Period = arr;                          /* 自动重装载值 */
    g_timx_npwm_chy_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; /*使能TIMx_ARR进行缓冲 */
    g_timx_npwm_chy_handle.Init.RepetitionCounter = 0;                 /* 重复计数器初始值 */
    HAL_TIM_PWM_Init(&g_timx_npwm_chy_handle);                         /* 初始化PWM */

    gpio_init_struct.Pin = ATIM_TIMX_NPWM_CHY_GPIO_PIN;                /* 通道y的CPIO口 */
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;                           /* 复用推完输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                               /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;                     /* 高速 */
    HAL_GPIO_Init(ATIM_TIMX_NPWM_CHY_GPIO_PORT, &gpio_init_struct);

    timx_oc_npwm_chy.OCMode = TIM_OCMODE_PWM1;                         /* 模式选择PWM 1*/
    timx_oc_npwm_chy.Pulse = arr / 2;                                  /* 设置比较值,此值用来确定占空比 */
                                                                       /* 这里默认设置比较值为自动重装载值的一半,即占空比为50% */
    timx_oc_npwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH;                 /* 输出比较极性为高 */
    HAL_TIM_PWM_ConfigChannel(&g_timx_npwm_chy_handle, &timx_oc_npwm_chy, ATIM_TIMX_NPWM_CHY); /* 配置TIMx通道y */

    HAL_NVIC_SetPriority(ATIM_TIMX_NPWM_IRQn, 1, 3);                   /* 设置中断优先级,抢占优先级1,子优先级3 */
    HAL_NVIC_EnableIRQ(ATIM_TIMX_NPWM_IRQn);                           /* 开启ITMx中断 */

    __HAL_TIM_ENABLE_IT(&g_timx_npwm_chy_handle, TIM_IT_UPDATE);       /* 允许更新中断 */
    HAL_TIM_PWM_Start(&g_timx_npwm_chy_handle, ATIM_TIMX_NPWM_CHY);    /* 开启对应PWM通道 */
}

/**
 * @brief       高级定时器TIMX NPWM设置PWM个数
 * @param       rcr: PWM的个数, 1~2^32次方个
 * @retval      无
 */
void atim_timx_npwm_chy_set(uint32_t npwm)
{
    if (npwm == 0) return;

    g_npwm_remain = npwm;                                                   /* 保存脉冲个数 */
    HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle, TIM_EVENTSOURCE_UPDATE); /* 产生一次更新事件,在中断里面处理脉冲输出 */
    __HAL_TIM_ENABLE(&g_timx_npwm_chy_handle);                              /* 使能定时器TIMX */
}

/**
 * @brief       高级定时器TIMX NPWM中断服务函数
 * @param       无
 * @retval      无
 */
void ATIM_TIMX_NPWM_IRQHandler(void)
{
    uint16_t npwm = 0;

    /* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式 */
    if(__HAL_TIM_GET_FLAG(&g_timx_npwm_chy_handle, TIM_FLAG_UPDATE) != RESET)
    {
        if (g_npwm_remain >= 256)           /* 还有大于256个脉冲需要发送 */
        {
            g_npwm_remain = g_npwm_remain - 256;
            npwm = 256;
        }
        else if (g_npwm_remain % 256)       /* 还有位数(不到256)个脉冲要发送 */
        {
            npwm = g_npwm_remain % 256;
            g_npwm_remain = 0;              /* 没有脉冲了 */
        }

        if (npwm) /* 有脉冲要发送 */
        {
            ATIM_TIMX_NPWM->RCR = npwm - 1;                                         /* 设置重复计数寄存器值为npwm-1, 即npwm个脉冲 */
            HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle, TIM_EVENTSOURCE_UPDATE); /* 产生一次更新事件,在中断里面处理脉冲输出 */
            __HAL_TIM_ENABLE(&g_timx_npwm_chy_handle);                              /* 使能定时器TIMX */
        }
        else
        {
            ATIM_TIMX_NPWM->CR1 &= ~(1 << 0); /* 关闭定时器TIMX,使用HAL Disable会清除PWM通道信息,此处不用 */
        }

        __HAL_TIM_CLEAR_IT(&g_timx_npwm_chy_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */
    }
}

输出比较模式,这个就是把 OCMode 配置为 TIM_OCMODE_TOGGLE 变成翻转功能;这样相当于占空比就是 50%,但是设置不同的 Pulse 能产生不同的相位

互补输出还要带死区,常用在电机控制,带死区就可以在切换高低电平的时候,插入一段时间,不会立马同时转换。一般是 CHx 先变,然后过了一段时间 CHxN 在切换。

那么除了一直用到现在的 TIM_HandleTypeDef 结构体之外,还有 TIM_BreakDeadTimeConfigTypeDef 这个结构体,配置断路和死区。

定时器和 PWM 都是老样子来完成配置,只不过要设置的除了 OCPolarity 还有 OCNPolarity 来控制两个通道的输出状态,以及 OCIdleState 和 OCNIdleState 空闲状态的输出情况;之后配置死区和刹车,运行和空闲模式都关闭输出,OffStateRunMode 和 OffStateIDLEMode 都是 TIM_OSSR_DISABLE 和 TIM_OSSI_DISABLE,然后寄存器锁功能关闭,使能刹车输入,刹车输入有效信号为高,且使能自动回复输出,最后调用 HAL_TIMEx_ConfigBreakDeadTime() 完成配置。

最重要的死区时间控制,就是设置 DeadTime 参数,计算公式参考表格,同时设置完之后需要 __HAL_TIM_MOE_ENABLE 才能使能主输出。

死区时间计算

代码如下:

TIM_HandleTypeDef g_timx_cplm_pwm_handle;                              /* 定时器x句柄 */
TIM_BreakDeadTimeConfigTypeDef g_sbreak_dead_time_config = {0};        /* 死区时间设置 */

/**
 * @brief       高级定时器TIMX 互补输出 初始化函数(使用PWM模式1)
 * @note
 *              配置高级定时器TIMX 互补输出, 一路OCy 一路OCyN, 并且可以设置死区时间
 *
 *              高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
 *              高级定时器时钟 = 72Mhz
 *              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
 *              Ft=定时器工作频率,单位:Mhz
 *
 * @param       arr: 自动重装值。
 * @param       psc: 时钟预分频数
 * @retval      无
 */

void atim_timx_cplm_pwm_init(uint16_t arr, uint16_t psc)
{
    GPIO_InitTypeDef gpio_init_struct = {0};
    TIM_OC_InitTypeDef tim_oc_cplm_pwm = {0};

    ATIM_TIMX_CPLM_CLK_ENABLE();            /* TIMx 时钟使能 */
    ATIM_TIMX_CPLM_CHY_GPIO_CLK_ENABLE();   /* 通道X对应IO口时钟使能 */
    ATIM_TIMX_CPLM_CHYN_GPIO_CLK_ENABLE();  /* 通道X互补通道对应IO口时钟使能 */
    ATIM_TIMX_CPLM_BKIN_GPIO_CLK_ENABLE();  /* 通道X刹车输入对应IO口时钟使能 */
  
    gpio_init_struct.Pin = ATIM_TIMX_CPLM_CHY_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_AF_PP; 
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH ;
    HAL_GPIO_Init(ATIM_TIMX_CPLM_CHY_GPIO_PORT, &gpio_init_struct);

    gpio_init_struct.Pin = ATIM_TIMX_CPLM_CHYN_GPIO_PIN;
    HAL_GPIO_Init(ATIM_TIMX_CPLM_CHYN_GPIO_PORT, &gpio_init_struct);

    gpio_init_struct.Pin = ATIM_TIMX_CPLM_BKIN_GPIO_PIN;
    HAL_GPIO_Init(ATIM_TIMX_CPLM_BKIN_GPIO_PORT, &gpio_init_struct);
    
    ATIM_TIMX_CPLM_CHYN_GPIO_REMAP();     /* 映射定时器IO,PE不是本例程所用定时器的默认IO,需要复用 */

    g_timx_cplm_pwm_handle.Instance = ATIM_TIMX_CPLM;                       /* 定时器x */
    g_timx_cplm_pwm_handle.Init.Prescaler = psc;                            /* 定时器预分频系数 */
    g_timx_cplm_pwm_handle.Init.CounterMode = TIM_COUNTERMODE_UP;           /* 递增计数模式 */
    g_timx_cplm_pwm_handle.Init.Period = arr;                               /* 自动重装载值 */
    g_timx_cplm_pwm_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV4;     /* CKD[1:0] = 10, tDTS = 4 * tCK_INT = Ft / 4 = 18Mhz */
    g_timx_cplm_pwm_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; /* 使能影子寄存器TIMx_ARR */
    HAL_TIM_PWM_Init(&g_timx_cplm_pwm_handle);

    tim_oc_cplm_pwm.OCMode = TIM_OCMODE_PWM1;                               /* PWM模式1 */
    tim_oc_cplm_pwm.OCPolarity = TIM_OCPOLARITY_LOW;                        /* OCy 低电平有效 */
    tim_oc_cplm_pwm.OCNPolarity = TIM_OCNPOLARITY_LOW;                      /* OCyN 低电平有效 */
    tim_oc_cplm_pwm.OCIdleState = TIM_OCIDLESTATE_SET;                      /* 当MOE=0,OCx=1 */
    tim_oc_cplm_pwm.OCNIdleState = TIM_OCNIDLESTATE_SET;                    /* 当MOE=0,OCxN=1 */
    HAL_TIM_PWM_ConfigChannel(&g_timx_cplm_pwm_handle, &tim_oc_cplm_pwm, ATIM_TIMX_CPLM_CHY);

    /* 设置死区参数,开启死区中断 */
    g_sbreak_dead_time_config.OffStateRunMode = TIM_OSSR_DISABLE;           /* 运行模式的关闭输出状态 */
    g_sbreak_dead_time_config.OffStateIDLEMode = TIM_OSSI_DISABLE;          /* 空闲模式的关闭输出状态 */
    g_sbreak_dead_time_config.LockLevel = TIM_LOCKLEVEL_OFF;                /* 不用寄存器锁功能 */
    g_sbreak_dead_time_config.BreakState = TIM_BREAK_ENABLE;                /* 使能刹车输入 */
    g_sbreak_dead_time_config.BreakPolarity = TIM_BREAKPOLARITY_HIGH;       /* 刹车输入有效信号极性为高 */
    g_sbreak_dead_time_config.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE; /* 使能AOE位,允许刹车结束后自动恢复输出 */
    HAL_TIMEx_ConfigBreakDeadTime(&g_timx_cplm_pwm_handle, &g_sbreak_dead_time_config);

    HAL_TIM_PWM_Start(&g_timx_cplm_pwm_handle, ATIM_TIMX_CPLM_CHY);         /* 使能OCy输出 */
    HAL_TIMEx_PWMN_Start(&g_timx_cplm_pwm_handle, ATIM_TIMX_CPLM_CHY);      /* 使能OCyN输出 */
}

/**
 * @brief       定时器TIMX 设置输出比较值 & 死区时间
 * @param       ccr: 输出比较值
 * @param       dtg: 死区时间
 *   @arg       dtg[7:5]=0xx时, 死区时间 = dtg[7:0] * tDTS
 *   @arg       dtg[7:5]=10x时, 死区时间 = (64 + dtg[6:0]) * 2  * tDTS
 *   @arg       dtg[7:5]=110时, 死区时间 = (32 + dtg[5:0]) * 8  * tDTS
 *   @arg       dtg[7:5]=111时, 死区时间 = (32 + dtg[5:0]) * 16 * tDTS
 *   @note      tDTS = 1 / (Ft /  CKD[1:0]) = 1 / 18M = 55.56ns
 * @retval      无
 */
void atim_timx_cplm_pwm_set(uint16_t ccr, uint8_t dtg)
{
    g_sbreak_dead_time_config.DeadTime = dtg;       /* 死区时间设置 */
    HAL_TIMEx_ConfigBreakDeadTime(&g_timx_cplm_pwm_handle, &g_sbreak_dead_time_config);  /* 重设死区时间 */
    __HAL_TIM_MOE_ENABLE(&g_timx_cplm_pwm_handle);  /* MOE=1,使能主输出 */
    ATIM_TIMX_CPLM_CHY_CCRY = ccr;                  /* 设置比较寄存器 */
}

OLED 屏幕

这个主要是 三线SPI 的一个时序模拟,其他都不用太多。

这个三线 SPI,就是 CS 片选信号,SCLK 串行时钟线,以及 SDIN 串行数据线,相当于 SPI 里面的 MOSI。模式相当于是 CPOL 和 CPHA 都是1,也就是第四种模式。四线的话就是多一个 DC 线,1是读写数据,0是读写命令。

针对 OLED 屏幕,用的是 SSD1306,所以是 128*64 bit 大小,就变成 8 页,每页都是128 * 8。**需要设置低字节和高字节起始地址。**低字节是 0x00-0x0F,高字节是 0x10-0x1F。

方便的方法,是自定义一个 GRAM,每次修改 GRAM,然后一次性把 GRAM 更新到 OLED 里面去

以下是 OLED 的常用命令:

常用命令

命令的使用,就是 SPI 先写好一个最基础的 WRITE 命令,然后调用这个命令协指令,写好了在写要写入的数据就可以了。

当然,要使用的话,还得自己做一个字库,这个现成的很多。

以下是关键代码:

/**
 * @brief       初始化OLED(SSD1306)
 * @param       无
 * @retval      无
 */
void oled_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
    __HAL_RCC_GPIOC_CLK_ENABLE();     /* 使能PORTC时钟 */
    __HAL_RCC_GPIOD_CLK_ENABLE();     /* 使能PORTD时钟 */
    __HAL_RCC_GPIOG_CLK_ENABLE();     /* 使能PORTG时钟 */

#if OLED_MODE==1    /* 使用8080并口模式 */

    /* PC0 ~ 7 设置 */
    gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;                
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;        /* 中速 */
    HAL_GPIO_Init(GPIOC, &gpio_init_struct);                /* PC0 ~ 7 设置 */

    gpio_init_struct.Pin = GPIO_PIN_3|GPIO_PIN_6;           /* PD3, PD6 设置 */
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;        /* 中速 */
    HAL_GPIO_Init(GPIOD, &gpio_init_struct);                /* PD3, PD6 设置 */
    
    gpio_init_struct.Pin = GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;        /* 中速 */
    HAL_GPIO_Init(GPIOG, &gpio_init_struct);                /* WR/RD/RST引脚模式设置 */

    OLED_WR(1);
    OLED_RD(1);

#else               /* 使用4线SPI 串口模式 */

    gpio_init_struct.Pin = OLED_SPI_RST_PIN;                /* RST引脚 */
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;        /* 中速 */
    HAL_GPIO_Init(OLED_SPI_RST_PORT, &gpio_init_struct);    /* RST引脚模式设置 */

    gpio_init_struct.Pin = OLED_SPI_CS_PIN;                 /* CS引脚 */
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;        /* 中速 */
    HAL_GPIO_Init(OLED_SPI_CS_PORT, &gpio_init_struct);     /* CS引脚模式设置 */

    gpio_init_struct.Pin = OLED_SPI_RS_PIN;                 /* RS引脚 */
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;        /* 中速 */
    HAL_GPIO_Init(OLED_SPI_RS_PORT, &gpio_init_struct);     /* RS引脚模式设置 */

    gpio_init_struct.Pin = OLED_SPI_SCLK_PIN;                /* SCLK引脚 */
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;             /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                     /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;         /* 中速 */
    HAL_GPIO_Init(OLED_SPI_SCLK_PORT, &gpio_init_struct);    /* SCLK引脚模式设置 */

    gpio_init_struct.Pin = OLED_SPI_SDIN_PIN;               /* SDIN引脚模式设置 */
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;        /* 中速 */
    HAL_GPIO_Init(OLED_SPI_SDIN_PORT, &gpio_init_struct);   /* SDIN引脚模式设置 */

    OLED_SDIN(1);
    OLED_SCLK(1);
#endif
    OLED_CS(1);
    OLED_RS(1);

    OLED_RST(0);
    delay_ms(100);
    OLED_RST(1);

    oled_wr_byte(0xAE, OLED_CMD);   /* 关闭显示 */
    oled_wr_byte(0xD5, OLED_CMD);   /* 设置时钟分频因子,震荡频率 */
    oled_wr_byte(80, OLED_CMD);     /* [3:0],分频因子;[7:4],震荡频率 */
    oled_wr_byte(0xA8, OLED_CMD);   /* 设置驱动路数 */
    oled_wr_byte(0X3F, OLED_CMD);   /* 默认0X3F(1/64) */
    oled_wr_byte(0xD3, OLED_CMD);   /* 设置显示偏移 */
    oled_wr_byte(0X00, OLED_CMD);   /* 默认为0 */

    oled_wr_byte(0x40, OLED_CMD);   /* 设置显示开始行 [5:0],行数. */

    oled_wr_byte(0x8D, OLED_CMD);   /* 电荷泵设置 */
    oled_wr_byte(0x14, OLED_CMD);   /* bit2,开启/关闭 */
    oled_wr_byte(0x20, OLED_CMD);   /* 设置内存地址模式 */
    oled_wr_byte(0x02, OLED_CMD);   /* [1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10; */
    oled_wr_byte(0xA1, OLED_CMD);   /* 段重定义设置,bit0:0,0->0;1,0->127; */
    oled_wr_byte(0xC8, OLED_CMD);   /* 设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数 */
    oled_wr_byte(0xDA, OLED_CMD);   /* 设置COM硬件引脚配置 */
    oled_wr_byte(0x12, OLED_CMD);   /* [5:4]配置 */

    oled_wr_byte(0x81, OLED_CMD);   /* 对比度设置 */
    oled_wr_byte(0xEF, OLED_CMD);   /* 1~255;默认0X7F (亮度设置,越大越亮) */
    oled_wr_byte(0xD9, OLED_CMD);   /* 设置预充电周期 */
    oled_wr_byte(0xf1, OLED_CMD);   /* [3:0],PHASE 1;[7:4],PHASE 2; */
    oled_wr_byte(0xDB, OLED_CMD);   /* 设置VCOMH 电压倍率 */
    oled_wr_byte(0x30, OLED_CMD);   /* [6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc; */

    oled_wr_byte(0xA4, OLED_CMD);   /* 全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) */
    oled_wr_byte(0xA6, OLED_CMD);   /* 设置显示方式;bit0:1,反相显示;0,正常显示 */
    oled_wr_byte(0xAF, OLED_CMD);   /* 开启显示 */
    oled_clear();
}

/**
 * @brief       向OLED写入一个字节
 * @param       data: 要输出的数据
 * @param       cmd: 数据/命令标志 0,表示命令;1,表示数据;
 * @retval      无
 */
static void oled_wr_byte(uint8_t data, uint8_t cmd)
{
    uint8_t i;
    OLED_RS(cmd);   /* 写命令 */
    OLED_CS(0);

    for (i = 0; i < 8; i++)
    {
        OLED_SCLK(0);

        if (data & 0x80)
        {
            OLED_SDIN(1);
        }
        else
        {
            OLED_SDIN(0);
        }

        OLED_SCLK(1);
        data <<= 1;
    }

    OLED_CS(1);
    OLED_RS(1);
}

/*
 * OLED的显存
 * 每个字节表示8个像素, 128,表示有128列, 8表示有64行, 高位表示高行数.
 * 比如:g_oled_gram[0][0],包含了第一列,第1~8行的数据. g_oled_gram[0][0].0,即表示坐标(0,0)
 * 类似的: g_oled_gram[1][0].1,表示坐标(1,1), g_oled_gram[10][1].2,表示坐标(10,10),
 *
 * 存放格式如下(高位表示高行数).
 * [0]0 1 2 3 ... 127
 * [1]0 1 2 3 ... 127
 * [2]0 1 2 3 ... 127
 * [3]0 1 2 3 ... 127
 * [4]0 1 2 3 ... 127
 * [5]0 1 2 3 ... 127
 * [6]0 1 2 3 ... 127
 * [7]0 1 2 3 ... 127
 */
static uint8_t g_oled_gram[128][8];

/**
 * @brief       更新显存到OLED
 * @param       无
 * @retval      无
 */
void oled_refresh_gram(void)
{
    uint8_t i, n;

    for (i = 0; i < 8; i++)
    {
        oled_wr_byte (0xb0 + i, OLED_CMD); /* 设置页地址(0~7) */
        oled_wr_byte (0x00, OLED_CMD);     /* 设置显示位置—列低地址 */
        oled_wr_byte (0x10, OLED_CMD);     /* 设置显示位置—列高地址 */

        for (n = 0; n < 128; n++)
        {
            oled_wr_byte(g_oled_gram[n][i], OLED_DATA);
        }
    }
}

还有一些画点什么的就不拉上来了。

DMA

可以自动搬运数据,不需要 CPU 的参与。F4 的要比 F1 的强大很多,但是我没用过 FIFO 这种,所以也就不清楚具体的了。

配置的话,需要 DMA_HandleTypeDef 结构体完成。例如用在串口发送上,先要配置好 USART,然后配置 DMA,时钟使能后 __HAL_LINKDMA 联系起来,然后初始化 DMA,配置传输方向,内存、外设地址是否增量,数据位宽,模式以及优先级,最后 HAL_DMA_Init 使能

代码如下:

DMA_HandleTypeDef  g_dma_handle;            /* DMA句柄 */
extern UART_HandleTypeDef g_uart1_handle;   /* UART句柄 */


/**
 * @brief       串口TX DMA初始化函数
 *   @note      这里的传输形式是固定的, 这点要根据不同的情况来修改
 *              从存储器 -> 外设模式/8位数据宽度/存储器增量模式
 *
 * @param       dmax_chy    : DMA的通道, DMA1_Channel1 ~ DMA1_Channel7, DMA2_Channel1 ~ DMA2_Channel5
 *                            某个外设对应哪个DMA, 哪个通道, 请参考<<STM32中文参考手册 V10>> 10.3.7节
 *                            必须设置正确的DMA及通道, 才能正常使用! 
 * @retval      无
 */
void dma_init(DMA_Channel_TypeDef* DMAx_CHx)
{
    if ((uint32_t)DMAx_CHx > (uint32_t)DMA1_Channel7)     /* 大于DMA1_Channel7, 则为DMA2的通道了 */
    {
        __HAL_RCC_DMA2_CLK_ENABLE();                      /* DMA2时钟使能 */
    }
    else 
    {
        __HAL_RCC_DMA1_CLK_ENABLE();                      /* DMA1时钟使能 */
    }
    
    __HAL_LINKDMA(&g_uart1_handle, hdmatx, g_dma_handle);           /* 将DMA与USART1联系起来(发送DMA) */
    
    /* Tx DMA配置 */
    g_dma_handle.Instance = DMAx_CHx;                               /* USART1_TX使用的DMA通道为: DMA1_Channel4 */
    g_dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH;             /* DIR = 1 , 存储器到外设模式 */
    g_dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;                 /* 外设非增量模式 */
    g_dma_handle.Init.MemInc = DMA_MINC_ENABLE;                     /* 存储器增量模式 */
    g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;    /* 外设数据长度:8位 */
    g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;       /* 存储器数据长度:8位 */
    g_dma_handle.Init.Mode = DMA_NORMAL;                            /* DMA模式:正常模式 */
    g_dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;               /* 中等优先级 */

    HAL_DMA_Init(&g_dma_handle);
}

这样,发送函数就可以用 HAL_UART_Transmit_DMA,可以用 __HAL_DMA_GET_COUNTER 看传输完成情况

如果是 F4,那么配置的时候还有 FIFOMode,FIFOThreashold,MemBurst以及PeriphBurst,对应了 FIFO 和突发传递功能,但是都没有用到,所以 DISABLE 和 SINGLE 不开启就行了。

ADC

注意,输入范围最大就是 3.3V,输入通道,ADC1 和 ADC2 一共是 16 个外部和 2 个内部,ADC3 则是 8 个外部。

ADC 通道表

转换顺序,分为规则组和注入组,可以等效认为是常规的和中断的,注入组可以打断规则组,注入组执行完了才执行原先的规则组

触发的话可以分为两种,ADON 位触发,以及外部触发。

转换时间,ADC 输入时钟是在 PCLK2 分频之后的,注意的是 ADC 输入最大是 14MHz,那么在 F1 中,就需要 6 分频;F4 的话最大是 36 MHz,所以 4 分频。时间,需要设置采样时间,太短就会导致采集的不平稳。

可以配置 DMA 请求,以及单词和连续转换,扫描模式和非扫描模式。

主要看看多通道DMA 是怎么完成的:

需要配置 ADC_HandleTypeDef 结构体,其中参数就是这个结构体之中的 ADC_InitTypeDef 结构体,设置对应的对齐模式,扫描模式,连续转换,通道数目等ADC_ChannelConfTypeDef 结构体,设置转换通道,转换顺序和采样周期。开启转换通过 HAL_ADC_Start,然后 HAL_ADC_PollForConversion 等待完成转换,HAL_ADC_GetValue 获取数值。

代码如下:

DMA_HandleTypeDef g_dma_nch_adc_handle = {0};                               /* 定义要搬运ADC多通道数据的DMA句柄 */
ADC_HandleTypeDef g_adc_nch_dma_handle = {0};                               /* 定义ADC(多通道DMA读取)句柄 */

/**
 * @brief       ADC N通道(6通道) DMA读取 初始化函数
 *   @note      本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置
 *              另外,由于本函数用到了6个通道, 宏定义会比较多内容, 因此,本函数就不采用宏定义的方式来修改通道了,
 *              直接在本函数里面修改, 这里我们默认使用PA0~PA5这6个通道.
 *
 *              注意: 本函数还是使用 ADC_ADCX(默认=ADC1) 和 ADC_ADCX_DMACx( DMA1_Channel1 ) 及其相关定义
 *              不要乱修改adc.h里面的这两部分内容, 必须在理解原理的基础上进行修改, 否则可能导致无法正常使用.
 *
 * @param       mar         : 存储器地址 
 * @retval      无
 */
void adc_nch_dma_init(uint32_t mar)
{
    GPIO_InitTypeDef gpio_init_struct;
    RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
    ADC_ChannelConfTypeDef adc_ch_conf = {0};

    ADC_ADCX_CHY_CLK_ENABLE();                                                /* 使能ADCx时钟 */
    __HAL_RCC_GPIOA_CLK_ENABLE();                                             /* 开启GPIOA时钟 */

    if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)                   /* 大于DMA1_Channel7, 则为DMA2的通道了 */
    {
        __HAL_RCC_DMA2_CLK_ENABLE();                                          /* DMA2时钟使能 */
    }
    else
    {
        __HAL_RCC_DMA1_CLK_ENABLE();                                          /* DMA1时钟使能 */
    }

    /* 设置ADC时钟 */
    adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;                    /* ADC外设时钟 */
    adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;                       /* 分频因子6时钟为72M/6=12MHz */
    HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                                 /* 设置ADC时钟 */

    /* 
        设置ADC1通道0~5对应的IO口模拟输入
        AD采集引脚模式设置,模拟输入
        PA0对应 ADC1_IN0
        PA1对应 ADC1_IN1
        PA2对应 ADC1_IN2
        PA3对应 ADC1_IN3
        PA4对应 ADC1_IN4
        PA5对应 ADC1_IN5
    */
    gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5;  /* GPIOA0~5 */
    gpio_init_struct.Mode = GPIO_MODE_ANALOG;                                 /* 模拟 */
    HAL_GPIO_Init(GPIOA, &gpio_init_struct);

    /* 初始化DMA */
    g_dma_nch_adc_handle.Instance = ADC_ADCX_DMACx;                           /* 设置DMA通道 */
    g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;               /* 从外设到存储器模式 */
    g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;                   /* 外设非增量模式 */
    g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE;                       /* 存储器增量模式 */
    g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  /* 外设数据长度:16位 */
    g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;     /* 存储器数据长度:16位 */
    g_dma_nch_adc_handle.Init.Mode = DMA_NORMAL;                              /* 外设流控模式 */
    g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;                 /* 中等优先级 */
    HAL_DMA_Init(&g_dma_nch_adc_handle);

    __HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_nch_adc_handle);   /* 将DMA与adc联系起来 */

    /* 初始化ADC */
    g_adc_nch_dma_handle.Instance = ADC_ADCX;                                 /* 选择哪个ADC */
    g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;                /* 数据对齐方式:右对齐 */
    g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;                 /* 使能扫描模式 */
    g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE;                    /* 使能连续转换 */
    g_adc_nch_dma_handle.Init.NbrOfConversion = 6;                            /* 赋值范围是1~16,本实验用到6个规则通道序列 */
    g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE;                /* 禁止规则通道组间断模式 */
    g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0;                        /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */
    g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;          /* 软件触发 */
    HAL_ADC_Init(&g_adc_nch_dma_handle);                                      /* 初始化 */

    HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle);                       /* 校准ADC */

    /* 配置ADC通道 */
    adc_ch_conf.Channel = ADC_CHANNEL_0;                                      /* 配置使用的ADC通道 */
    adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                                    /* 采样序列里的第1个 */
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;                    /* 采样时间,设置最大采样周期:239.5个ADC周期 */
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 通道配置 */
    
    adc_ch_conf.Channel = ADC_CHANNEL_1;                                      /* 配置使用的ADC通道 */
    adc_ch_conf.Rank = ADC_REGULAR_RANK_2;                                    /* 采样序列里的第2个 */
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */

    adc_ch_conf.Channel = ADC_CHANNEL_2;                                      /* 配置使用的ADC通道 */
    adc_ch_conf.Rank = ADC_REGULAR_RANK_3;                                    /* 采样序列里的第3个 */
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */

    adc_ch_conf.Channel = ADC_CHANNEL_3;                                      /* 配置使用的ADC通道 */
    adc_ch_conf.Rank = ADC_REGULAR_RANK_4;                                    /* 采样序列里的第4个 */
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */

    adc_ch_conf.Channel = ADC_CHANNEL_4;                                      /* 配置使用的ADC通道 */
    adc_ch_conf.Rank = ADC_REGULAR_RANK_5;                                    /* 采样序列里的第5个 */
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */

    adc_ch_conf.Channel = ADC_CHANNEL_5;                                      /* 配置使用的ADC通道 */
    adc_ch_conf.Rank = ADC_REGULAR_RANK_6;                                    /* 采样序列里的第6个 */
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */

    /* 配置DMA数据流请求中断优先级 */
    HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);
    HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);

    HAL_DMA_Start_IT(&g_dma_nch_adc_handle, (uint32_t)&ADC1->DR, mar, 0);     /* 启动DMA,并开启中断 */
    HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0);                        /* 开启ADC,通过DMA传输结果 */
}

/*************************单通道ADC采集(DMA读取)实验和多通道ADC采集(DMA读取)实验公用代码*******************************/

/**
 * @brief       使能一次ADC DMA传输
 *   @note      该函数用寄存器来操作,防止用HAL库操作对其他参数有修改,也为了兼容性
 * @param       ndtr: DMA传输的次数
 * @retval      无
 */
void adc_dma_enable(uint16_t cndtr)
{
    ADC_ADCX->CR2 &= ~(1 << 0);                 /* 先关闭ADC */

    ADC_ADCX_DMACx->CCR &= ~(1 << 0);           /* 关闭DMA传输 */
    while (ADC_ADCX_DMACx->CCR & (1 << 0));     /* 确保DMA可以被设置 */
    ADC_ADCX_DMACx->CNDTR = cndtr;              /* DMA传输数据量 */
    ADC_ADCX_DMACx->CCR |= 1 << 0;              /* 开启DMA传输 */

    ADC_ADCX->CR2 |= 1 << 0;                    /* 重新启动ADC */
    ADC_ADCX->CR2 |= 1 << 22;                   /* 启动规则转换通道 */
}

/**
 * @brief       ADC DMA采集中断服务函数
 * @param       无 
 * @retval      无
 */
void ADC_ADCX_DMACx_IRQHandler(void)
{
    if (ADC_ADCX_DMACx_IS_TC())
    {
        g_adc_dma_sta = 1;                      /* 标记DMA传输完成 */
        ADC_ADCX_DMACx_CLR_TC();                /* 清除DMA1 数据流7 传输完成中断 */
    }
}

注意的是,每一次进了中断需要进行对应的操作;配置的时候需要注意好对应的通道,因为是一次性转换多个通道然后循环的,之后要去 main 函数读取。

这里就看一下,电机开发那一块写的更简单,直接连续扫描模式很方便。

IIC

IIC 两根线,SDA 和 SCL 的串行总线,每个期间有唯一地址,双向电路,都需要连接空闲高电平,标准模式 100 kbit/s,可以多个主机和多个从机。注意,IIC 是半双工通信

时序图如下:

IIC 时序图

需要注意的是,SCL 和 SDA 都是高电平,那就是 IIC 的空闲状态。起始信号和停止信号比较简单;应答信号,就是脉冲 9 期间低电平为 ACK,高电平为 NACK,表示接收器接受该字节没成功。

写操作如下:

IIC 的写操作

从图中就可以看到,需要先发送的是 0 的写操作的从机地址(也就是从机地址 + 0),然后收到 ACK 之后就可以发送数据了。

读操作如下:

IIC 的读操作

这里一开始发送的就是 1 的读操作的从机地址(从机地址 + 1),然后从机就会发数据,这个时候,主机一直 ACK 那就一直收数据,直到主机 NACK,最后停止读取

代码设置的技巧:SDA 线设置成开漏输出,这样就可以读取外部的高低电平。一些关键代码如下:

/**
 * @brief       初始化IIC
 * @param       无
 * @retval      无
 */
void iic_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;

    IIC_SCL_GPIO_CLK_ENABLE();  /* SCL引脚时钟使能 */
    IIC_SDA_GPIO_CLK_ENABLE();  /* SDA引脚时钟使能 */

    gpio_init_struct.Pin = IIC_SCL_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;        /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */
    HAL_GPIO_Init(IIC_SCL_GPIO_PORT, &gpio_init_struct);/* SCL */

    gpio_init_struct.Pin = IIC_SDA_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;        /* 开漏输出 */
    HAL_GPIO_Init(IIC_SDA_GPIO_PORT, &gpio_init_struct);/* SDA */
    /* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */

    iic_stop();     /* 停止总线上所有设备 */
}

/**
 * @brief       IIC延时函数,用于控制IIC读写速度
 * @param       无
 * @retval      无
 */
static void iic_delay(void)
{
    delay_us(2);    /* 2us的延时, 读写速度在250Khz以内 */
}

/**
 * @brief       产生IIC起始信号
 * @param       无
 * @retval      无
 */
void iic_start(void)
{
    IIC_SDA(1);
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(0);     /* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
    iic_delay();
    IIC_SCL(0);     /* 钳住I2C总线,准备发送或接收数据 */
    iic_delay();
}

/**
 * @brief       产生IIC停止信号
 * @param       无
 * @retval      无
 */
void iic_stop(void)
{
    IIC_SDA(0);     /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
    iic_delay();
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(1);     /* 发送I2C总线结束信号 */
    iic_delay();
}

/**
 * @brief       等待应答信号到来
 * @param       无
 * @retval      1,接收应答失败
 *              0,接收应答成功
 */
uint8_t iic_wait_ack(void)
{
    uint8_t waittime = 0;
    uint8_t rack = 0;

    IIC_SDA(1);     /* 主机释放SDA线(此时外部器件可以拉低SDA线) */
    iic_delay();
    IIC_SCL(1);     /* SCL=1, 此时从机可以返回ACK */
    iic_delay();

    while (IIC_READ_SDA)    /* 等待应答 */
    {
        waittime++;

        if (waittime > 250)
        {
            iic_stop();
            rack = 1;
            break;
        }
    }

    IIC_SCL(0);     /* SCL=0, 结束ACK检查 */
    iic_delay();
    return rack;
}

/**
 * @brief       产生ACK应答
 * @param       无
 * @retval      无
 */
void iic_ack(void)
{
    IIC_SDA(0);     /* SCL 0 -> 1  时 SDA = 0,表示应答 */
    iic_delay();
    IIC_SCL(1);     /* 产生一个时钟 */
    iic_delay();
    IIC_SCL(0);
    iic_delay();
    IIC_SDA(1);     /* 主机释放SDA线 */
    iic_delay();
}

/**
 * @brief       不产生ACK应答
 * @param       无
 * @retval      无
 */
void iic_nack(void)
{
    IIC_SDA(1);     /* SCL 0 -> 1  时 SDA = 1,表示不应答 */
    iic_delay();
    IIC_SCL(1);     /* 产生一个时钟 */
    iic_delay();
    IIC_SCL(0);
    iic_delay();
}

/**
 * @brief       IIC发送一个字节
 * @param       data: 要发送的数据
 * @retval      无
 */
void iic_send_byte(uint8_t data)
{
    uint8_t t;
    
    for (t = 0; t < 8; t++)
    {
        IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */
        iic_delay();
        IIC_SCL(1);
        iic_delay();
        IIC_SCL(0);
        data <<= 1;     /* 左移1位,用于下一次发送 */
    }
    IIC_SDA(1);         /* 发送完成, 主机释放SDA线 */
}

/**
 * @brief       IIC读取一个字节
 * @param       ack:  ack=1时,发送ack; ack=0时,发送nack
 * @retval      接收到的数据
 */
uint8_t iic_read_byte(uint8_t ack)
{
    uint8_t i, receive = 0;

    for (i = 0; i < 8; i++ )    /* 接收1个字节数据 */
    {
        receive <<= 1;  /* 高位先输出,所以先收到的数据位要左移 */
        IIC_SCL(1);
        iic_delay();

        if (IIC_READ_SDA)
        {
            receive++;
        }
        
        IIC_SCL(0);
        iic_delay();
    }

    if (!ack)
    {
        iic_nack();     /* 发送nACK */
    }
    else
    {
        iic_ack();      /* 发送ACK */
    }

    return receive;
}

SPI

SPI 的话也是一个串行总线,一般有四个引脚:

  • MISO(Master In / Slave Out)主设备数据输入,从设备数据输出。
  • MOSI(Master Out / Slave In)主设备数据输出,从设备数据输入。
  • SCLK(Serial Clock)时钟信号,由主设备产生。
  • CS(Chip Select)从设备片选信号,由主设备产生。

同时,SPI 是可以选择全双工、半双工和单工的工作方式的。

四种工作模式,主要就看 CPOL 和 CPHA,CPOL 就是控制时钟极性,管的是 SCL 线,空闲高电平那就 CPOL = 1,反之为 0;CPHA 是采样时刻,第 1 个边沿信号采样就是 CPHA = 0,第 2 个就是 CPHA = 1。模式 0 和模式 3 比较多。

SPI 工作模式表

如果直接用硬件的话,需要的是 SPI_HandleTypeDef结构体,里面的 Init 就是相关的初始化配置,是 SPI_InitTypeDef 结构体。

代码如下:

/**
 * @brief       SPI初始化代码
 *   @note      主机模式,8位数据,禁止硬件片选
 * @param       无
 * @retval      无
 */
void spi2_init(void)
{
    SPI2_SPI_CLK_ENABLE(); /* SPI2时钟使能 */

    g_spi2_handler.Instance = SPI2_SPI;                                /* SPI2 */
    g_spi2_handler.Init.Mode = SPI_MODE_MASTER;                        /* 设置SPI工作模式,设置为主模式 */
    g_spi2_handler.Init.Direction = SPI_DIRECTION_2LINES;              /* 设置SPI单向或者双向的数据模式:SPI设置为双线模式 */
    g_spi2_handler.Init.DataSize = SPI_DATASIZE_8BIT;                  /* 设置SPI的数据大小:SPI发送接收8位帧结构 */
    g_spi2_handler.Init.CLKPolarity = SPI_POLARITY_HIGH;               /* 串行同步时钟的空闲状态为高电平 */
    g_spi2_handler.Init.CLKPhase = SPI_PHASE_2EDGE;                    /* 串行同步时钟的第二个跳变沿(上升或下降)数据被采样 */
    g_spi2_handler.Init.NSS = SPI_NSS_SOFT;                            /* NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制 */
    g_spi2_handler.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /* 定义波特率预分频的值:波特率预分频值为256 */
    g_spi2_handler.Init.FirstBit = SPI_FIRSTBIT_MSB;                   /* 指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始 */
    g_spi2_handler.Init.TIMode = SPI_TIMODE_DISABLE;                   /* 关闭TI模式 */
    g_spi2_handler.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;   /* 关闭硬件CRC校验 */
    g_spi2_handler.Init.CRCPolynomial = 7;                             /* CRC值计算的多项式 */
    HAL_SPI_Init(&g_spi2_handler);                                     /* 初始化 */

    __HAL_SPI_ENABLE(&g_spi2_handler); /* 使能SPI2 */

    spi2_read_write_byte(0Xff); /* 启动传输, 实际上就是产生8个时钟脉冲, 达到清空DR的作用, 非必需 */
}

/**
 * @brief       SPI底层驱动,时钟使能,引脚配置
 *   @note      此函数会被HAL_SPI_Init()调用
 * @param       hspi:SPI句柄
 * @retval      无
 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
    GPIO_InitTypeDef gpio_init_struct;
    
    if (hspi->Instance == SPI2_SPI)
    {
        SPI2_SCK_GPIO_CLK_ENABLE();  /* SPI2_SCK脚时钟使能 */
        SPI2_MISO_GPIO_CLK_ENABLE(); /* SPI2_MISO脚时钟使能 */
        SPI2_MOSI_GPIO_CLK_ENABLE(); /* SPI2_MOSI脚时钟使能 */

        /* SCK引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_SCK_GPIO_PIN;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;
        gpio_init_struct.Pull = GPIO_PULLUP;
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(SPI2_SCK_GPIO_PORT, &gpio_init_struct);

        /* MISO引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_MISO_GPIO_PIN;
        HAL_GPIO_Init(SPI2_MISO_GPIO_PORT, &gpio_init_struct);

        /* MOSI引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_MOSI_GPIO_PIN;
        HAL_GPIO_Init(SPI2_MOSI_GPIO_PORT, &gpio_init_struct);
    }
}

/**
 * @brief       SPI2速度设置函数
 *   @note      SPI2时钟选择来自APB1, 即PCLK1, 为36Mhz
 *              SPI速度 = PCLK1 / 2^(speed + 1)
 * @param       speed   : SPI2时钟分频系数
                        取值为SPI_BAUDRATEPRESCALER_2~SPI_BAUDRATEPRESCALER_2 256
 * @retval      无
 */
void spi2_set_speed(uint8_t speed)
{
    assert_param(IS_SPI_BAUDRATE_PRESCALER(speed)); /* 判断有效性 */
    __HAL_SPI_DISABLE(&g_spi2_handler);             /* 关闭SPI */
    g_spi2_handler.Instance->CR1 &= 0XFFC7;         /* 位3-5清零,用来设置波特率 */
    g_spi2_handler.Instance->CR1 |= speed << 3;     /* 设置SPI速度 */
    __HAL_SPI_ENABLE(&g_spi2_handler);              /* 使能SPI */
}

/**
 * @brief       SPI2读写一个字节数据
 * @param       txdata  : 要发送的数据(1字节)
 * @retval      接收到的数据(1字节)
 */
uint8_t spi2_read_write_byte(uint8_t txdata)
{
    uint8_t rxdata;
    HAL_SPI_TransmitReceive(&g_spi2_handler, &txdata, &rxdata, 1, 1000);
    return rxdata; /* 返回收到的数据 */
}

这里对于速度,是直接操作寄存器来完成配置。如果是发送,那就是 HAL_SPI_Transmit 发送。配置速度,就是修改 CR1 寄存器,配置的是 [5:3] 这几个位,设置波特率,工程里是 0,直接是时钟线 / 2,也就是 18MHz

CAN

CAN 协议的特点记一记,也很常问:

  • 多主控制;
  • 系统的柔软性;添加设备不影响已有的系统;
  • 通信速度快,距离远;最高 1Mbps(距离小于40m),最远 10km(速率低于 5kbps);
  • 具有错误检测、错误通知和错误恢复功能;
  • 故障封闭功能;
  • 连接节点多。

连接的时候,总线两端各串联 120Ω 电阻。采用的是差分信号,显性电平就是有电压差,隐性电平没有。

CAN 物理层特性

CAN 协议有 5 个帧类型:数据帧、遥控帧、错误帧、过载帧、间隔帧。数据帧以及遥控帧又分为标准格式和扩展格式。

CAN 帧类型

一般就是用数据帧来完成数据传输。数据帧有 7 个段:帧起始、仲裁段、控制段、数据段、CRC 段、ACK 段、帧结束。

数据帧构成

  • 帧起始1 个位的显性电平
  • 仲裁段标准 ID 就是 11 位,扩展 ID 有 29 位;禁止高 7 位都是隐性;RTR 用于表示是否远程帧,IDE则是区分标准帧和扩展帧(0 是标准帧);如果扩展帧那 SRR 代替远程帧就是隐性位;
  • 控制段:6 个位,表示数据段字节数,前 2 个位都是显性电平,之后高位在前,有效值为 0-8,但是接收方是都可以接收的;
  • 数据段:0-8 个字节数据,最高位开始输出,也就是 MSB;
  • CRC 段:检查帧传输错误,15 个位的 CRC 顺序和 1 个位的 CRC 界定符;计算包括:帧起始、仲裁段、控制段、数据段;
  • ACK 段:确认是否正常接收,由 ACK Slot 和 ACK 界定符 2 个位组成;发送就是 2 个隐性位,接收正确那么 ACK 槽显性
  • 帧结束7 个位的隐性位

CAN 的位时序,一个位有 4 段:

  • 同步段( SS)
  • 传播时间段( PTS)
  • 相位缓冲段 1 (PBS1)
  • 相位缓冲段 2 (PBS2)

这些段由最小时间单位 Tq 构成。各段定义如下:

一个位各段作用

实际采样点,是在 PBS1 结束的时候去完成的,那么根据这个位时序就有计算波特率了。总线仲裁的话,就是最先发送消息的获得发送权,如果同时,那就是从仲裁段第一位开始进行仲裁,连续输出显性电平最多的可继续发送

CAN 总线仲裁

实际的硬件 CAN,F1 和 F4 都是 bxCAN,基本扩展 CAN,支持 2.0A 以及 2.0B。CAN2.0A 只能标准数据帧,扩展帧会标为错误;CAN2.0B Active 可以处理标准数据帧和扩展数据帧;CAN2.0B Passive 只能处理标准数据帧,扩展帧会忽略。

波特率最高 1Mbps,支持时间触发通信,具有 3 个发送邮箱,3 级深度的 2 个接收 FIFO,可变的过滤器组(最多 28 个)。F1 只有 1 个 CAN,F4 有 2 个。

标识符过滤,最多是 28 个,F1 这个只有 14 个,F4 是 28 个,每个过滤器组是 2 个 32 位寄存器,CAN_FxR1 和 CAN_FxR2,位宽如下:

  • 1个 32位过滤器,包括: STDID[10:0]、 EXTID[17:0]、 IDE和 RTR位
  • 2个 16位过滤器,包括: STDID[10:0]、 IDE、 RTR和 EXTID[17:15]位

配置 CAN_FMR 寄存器,可设置过滤器组位宽和工作模式:过滤出 1 组标识符,应该设置屏蔽位模式;过滤出 1 个标识符,设置为标识符列表模式。如果设置 32 位过滤器-标识符屏蔽,CAN_F0R1 的值就是期望 ID,CAN_F0R2 就是必须关心的 ID,其中 [31:24] 和 [15:8] 必须和 CAN_F0R1 中一样,其余无所谓,但是 IDE 和 RTR 必须一致。

波特率:STM32把传播时间段和相位缓冲段 1(STM32称之为时间段 1)合并了,所以 STM32 的 CAN 一个位只有 3段:同步段( SYNC_SEG)、时间段 1(BS1)和时间段 2(BS2)。

STM32 中的 CAN 位时序

实际使用,初始化需要 CAN_HandleTypeDef 结构体,其中通过的 Init 就是配置的结构体,是 CAN_InitTypeDef 结构体。之后,还需要 CAN_FilterTypeDef 配置过滤器。HAL_CAN_Start 开启 CAN,HAL_CAN_ActivateNotification 使能中断,HAL_CAN_AddTxMessage 向发送邮箱添加报文,HAL_CAN_GetRxMessage 从 FIFO 接收报文。

代码如下:

CAN_HandleTypeDef   g_canx_handler;     /* CANx句柄 */
CAN_TxHeaderTypeDef g_canx_txheader;    /* 发送参数句柄 */
CAN_RxHeaderTypeDef g_canx_rxheader;    /* 接收参数句柄 */

/**
 * @brief       CAN初始化
 * @param       tsjw    : 重新同步跳跃时间单元.范围: 1~3;
 * @param       tbs2    : 时间段2的时间单元.范围: 1~8;
 * @param       tbs1    : 时间段1的时间单元.范围: 1~16;
 * @param       brp     : 波特率分频器.范围: 1~1024;
 *   @note      以上4个参数, 在函数内部会减1, 所以, 任何一个参数都不能等于0
 *              CAN挂在APB1上面, 其输入时钟频率为 Fpclk1 = PCLK1 = 36Mhz
 *              tq     = brp * tpclk1;
 *              波特率 = Fpclk1 / ((tbs1 + tbs2 + 1) * brp);
 *              我们设置 can_init(1, 8, 9, 4, 1), 则CAN波特率为:
 *              36M / ((8 + 9 + 1) * 4) = 500Kbps
 *
 * @param       mode    : CAN_MODE_NORMAL,  正常模式;
                          CAN_MODE_LOOPBACK,回环模式;
 * @retval      0,  初始化成功; 其他, 初始化失败;
 */
uint8_t can_init(uint32_t tsjw, uint32_t tbs2, uint32_t tbs1, uint16_t brp, uint32_t mode)
{
  g_canx_handler.Instance = CAN1;
  g_canx_handler.Init.Prescaler = brp;                /* 分频系数(Fdiv)为brp+1 */
  g_canx_handler.Init.Mode = mode;                    /* 模式设置 */
  g_canx_handler.Init.SyncJumpWidth = tsjw;           /* 重新同步跳跃宽度(Tsjw)为tsjw+1个时间单位 CAN_SJW_1TQ~CAN_SJW_4TQ */
  g_canx_handler.Init.TimeSeg1 = tbs1;                /* tbs1范围CAN_BS1_1TQ~CAN_BS1_16TQ */
  g_canx_handler.Init.TimeSeg2 = tbs2;                /* tbs2范围CAN_BS2_1TQ~CAN_BS2_8TQ */
  g_canx_handler.Init.TimeTriggeredMode = DISABLE;    /* 非时间触发通信模式 */
  g_canx_handler.Init.AutoBusOff = DISABLE;           /* 软件自动离线管理 */
  g_canx_handler.Init.AutoWakeUp = DISABLE;           /* 睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位) */
  g_canx_handler.Init.AutoRetransmission = ENABLE;    /* 禁止报文自动传送 */
  g_canx_handler.Init.ReceiveFifoLocked = DISABLE;    /* 报文不锁定,新的覆盖旧的 */
  g_canx_handler.Init.TransmitFifoPriority = DISABLE; /* 优先级由报文标识符决定 */
    
  if (HAL_CAN_Init(&g_canx_handler) != HAL_OK)
  {
    return 1;
  }

#if CAN_RX0_INT_ENABLE
  /* 使用中断接收 */
  __HAL_CAN_ENABLE_IT(&g_canx_handler, CAN_IT_RX_FIFO0_MSG_PENDING); /* FIFO0消息挂号中断允许 */
  HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);                          /* 使能CAN中断 */
  HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 1, 0);                  /* 抢占优先级1,子优先级0 */
#endif

  CAN_FilterTypeDef sFilterConfig;

  /*配置CAN过滤器*/
  sFilterConfig.FilterBank = 0;                         /* 过滤器0 */
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;     /* 标识符屏蔽位模式 */
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;    /* 长度32位位宽*/
  sFilterConfig.FilterIdHigh = 0x0000;                  /* 32位ID */
  sFilterConfig.FilterIdLow = 0x0000;
  sFilterConfig.FilterMaskIdHigh = 0x0000;              /* 32位MASK */
  sFilterConfig.FilterMaskIdLow = 0x0000;
  sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0;    /* 过滤器0关联到FIFO0 */
  sFilterConfig.FilterActivation = CAN_FILTER_ENABLE;       /* 激活滤波器0 */
  sFilterConfig.SlaveStartFilterBank = 14;

  /* 过滤器配置 */
  if (HAL_CAN_ConfigFilter(&g_canx_handler, &sFilterConfig) != HAL_OK)
  {
    return 2;
  }

  /* 启动CAN外围设备 */
  if (HAL_CAN_Start(&g_canx_handler) != HAL_OK)
  {
    return 3;
  }

  return 0;
}

/**
 * @brief       CAN底层驱动,引脚配置,时钟配置,中断配置
                此函数会被HAL_CAN_Init()调用
 * @param       hcan:CAN句柄
 * @retval      无
 */
void HAL_CAN_MspInit(CAN_HandleTypeDef *hcan)
{
  if (CAN1 == hcan->Instance)
  {
    CAN_RX_GPIO_CLK_ENABLE();       /* CAN_RX脚时钟使能 */
    CAN_TX_GPIO_CLK_ENABLE();       /* CAN_TX脚时钟使能 */
    __HAL_RCC_CAN1_CLK_ENABLE();    /* 使能CAN1时钟 */

    GPIO_InitTypeDef gpio_initure;

    gpio_initure.Pin = CAN_TX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_PP;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(CAN_TX_GPIO_PORT, &gpio_initure); /* CAN_TX脚 模式设置 */

    gpio_initure.Pin = CAN_RX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_INPUT;
    HAL_GPIO_Init(CAN_RX_GPIO_PORT, &gpio_initure); /* CAN_RX脚 必须设置成输入模式 */
  }
}

#if CAN_RX0_INT_ENABLE /* 使能RX0中断 */

/**
 * @brief       CAN RX0 中断服务函数
 *   @note      处理CAN FIFO0的接收中断
 * @param       无
 * @retval      无
 */
void USB_LP_CAN1_RX0_IRQHandler(void)
{
  uint8_t rxbuf[8];
  uint32_t id;
  uint8_t ide, rtr, len;
    
  can_receive_msg(id, rxbuf);
  printf("id:%d\r\n", g_canx_rxheader.StdId);
  printf("ide:%d\r\n", g_canx_rxheader.IDE);
  printf("rtr:%d\r\n", g_canx_rxheader.RTR);
  printf("len:%d\r\n", g_canx_rxheader.DLC);
  printf("rxbuf[0]:%d\r\n", rxbuf[0]);
  printf("rxbuf[1]:%d\r\n", rxbuf[1]);
  printf("rxbuf[2]:%d\r\n", rxbuf[2]);
  printf("rxbuf[3]:%d\r\n", rxbuf[3]);
  printf("rxbuf[4]:%d\r\n", rxbuf[4]);
  printf("rxbuf[5]:%d\r\n", rxbuf[5]);
  printf("rxbuf[6]:%d\r\n", rxbuf[6]);
  printf("rxbuf[7]:%d\r\n", rxbuf[7]);
}

#endif

/**
 * @brief       CAN 发送一组数据
 *   @note      发送格式固定为: 标准ID, 数据帧
 * @param       id      : 标准ID(11位)
 * @retval      发送状态 0, 成功; 1, 失败;
 */
uint8_t can_send_msg(uint32_t id, uint8_t *msg, uint8_t len)
{
  uint32_t TxMailbox = CAN_TX_MAILBOX0;
    
  g_canx_txheader.StdId = id;         /* 标准标识符 */
  g_canx_txheader.ExtId = id;         /* 扩展标识符(29位) 标准标识符情况下,该成员无效*/
  g_canx_txheader.IDE = CAN_ID_STD;   /* 使用标准标识符 */
  g_canx_txheader.RTR = CAN_RTR_DATA; /* 数据帧 */
  g_canx_txheader.DLC = len;

  if (HAL_CAN_AddTxMessage(&g_canx_handler, &g_canx_txheader, msg, &TxMailbox) != HAL_OK) /* 发送消息 */
  {
    return 1;
  }
  
  while (HAL_CAN_GetTxMailboxesFreeLevel(&g_canx_handler) != 3); /* 等待发送完成,所有邮箱(有三个邮箱)为空 */
  
  return 0;
}

/**
 * @brief       CAN 接收数据查询
 *   @note      接收数据格式固定为: 标准ID, 数据帧
 * @param       id      : 要查询的 标准ID(11位)
 * @param       buf     : 数据缓存区
 * @retval      接收结果
 *   @arg       0   , 无数据被接收到;
 *   @arg       其他, 接收的数据长度
 */
uint8_t can_receive_msg(uint32_t id, uint8_t *buf)
{
  if (HAL_CAN_GetRxFifoFillLevel(&g_canx_handler, CAN_RX_FIFO0) == 0)     /* 没有接收到数据 */
  {
    return 0;
  }

  if (HAL_CAN_GetRxMessage(&g_canx_handler, CAN_RX_FIFO0, &g_canx_rxheader, buf) != HAL_OK)  /* 读取数据 */
  {
    return 0;
  }
  
  if (g_canx_rxheader.StdId!= id || g_canx_rxheader.IDE != CAN_ID_STD || g_canx_rxheader.RTR != CAN_RTR_DATA)       /* 接收到的ID不对 / 不是标准帧 / 不是数据帧 */
  {
    return 0;    
  }

  return g_canx_rxheader.DLC;
}

常用的升级:IAP

这个就是 BootLoader 相关的内容。

STM32可以通过设置 MSP 的方式从不同的地址启动:包括 Flash 地址 、 RAM 地址等, 在默认方式下,我们的嵌入式程序是以连续二进制的方式烧录到 STM32 的可寻址 Flash 区域上的

IAP 的话,第一个程序检查有无升级需求,一般通过 USB/USART 接收程序,然后执行对第二部分代码的更新;第二部分就是真正的功能代码。这两部分同时烧录在 User Flash 中,芯片上电就是第一部分:

  • 检查是否需要更新
  • 不更新直接跳转第二部分
  • 执行更新
  • 执行第二部分

第一部分一般是 ISP 方法烧录,然后就不变了;第二部分是 IAP 烧写。

第一个项目代码称之为 Bootloader 程序,第二个项目代码称之为 APP 程序,他们存放在 STM32F103内部 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是 APP程序。这样就是要实现 2个程序: Bootloader和 APP。STM32F1的 APP 程序不仅可以放到 FLASH 里面运行,也可以放到 SRAM 里面运行。

这里看看正常的和带有 IAP 的两种不同运行流程:

正常运行

加入 IAP

需要注意,0x08000004 去出复位中断向量地址,跳转到复位中断服务程序,运行完后跳转到 IAP 的 main 函数,执行后(新 APP 写入 FLASH,新复位中断向量起始地址为 0X08000004+N+M),跳转并去除新程序的复位中断向量地址,跳转执行新程序的复位中断服务函数,随后跳转到新的 main 函数。CPU 有中断请求后,PC 指针会跳转 0x08000004 中断向量表处,再根据设置好的中断向量表偏移量,跳转到对应的中断服务函数,执行后回到 main

设置 APP 起始地址:IROM1 的 Start 一般是 0x08000000,大小是 0x80000;也就是 0x08000000 开始的 512KB 是程序存储区。现在修改为 Start 在 0x08010000,偏移量为 0x10000(64KB,也就是 BootLoader 的空间),那么留给 APP 的 FLASH 空间是 0x70000(448KB)。(这里要注意,APP 起始需要在 BootLoader 结束位置之后,且偏移量是 0x200 倍数

中断向量表,默认是 BOOT 启动模式决定,也就是指向 0x08000000,不过可以调用 sys_nvic_set_vector_table 实现重定向:

/**
 * @brief       设置中断向量表偏移地址
 * @param       baseaddr: 基址
 * @param       offset: 偏移量(必须是0, 或者0X100的倍数)
 * @retval      无
 */
void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset)
{
    /* 设置NVIC的向量表偏移寄存器,VTOR低9位保留,即[8:0]保留 */
    SCB->VTOR = baseaddr | (offset & (uint32_t)0xFFFFFE00);
}

之后,APP 的话,MDK 默认是 hex 文件,但是 IAP 需要的是 bin 文件。只要在 MDK 里面加上对应命令就可以了。在魔术棒的 User 中,Rebuild 一栏加上命令:

fromelf  --bin -o ..\..\Output\@L.bin  ..\..\Output\%L

然后,APP 程序设置起始位置和大小,然后调用函数实现中断向量表偏移量设置,设置好之后编译生成 bin 就可以了。

代码如下:

iapfun jump2app;
uint16_t g_iapbuf[1024];       /* 2K字节缓存 */

/**
 * @brief       IAP写入APP BIN
 * @param       appxaddr : 应用程序的起始地址
 * @param       appbuf   : 应用程序CODE
 * @param       appsize  : 应用程序大小(字节)
 * @retval      无
 */
void iap_write_appbin(uint32_t appxaddr, uint8_t *appbuf, uint32_t appsize)
{
    uint16_t t;
    uint16_t i = 0;
    uint16_t temp;
    uint32_t fwaddr = appxaddr; /* 当前写入的地址 */
    uint8_t *dfu = appbuf;

    for (t = 0; t < appsize; t += 2)
    {
        temp = (uint16_t)dfu[1] << 8;
        temp |= (uint16_t)dfu[0];
        dfu += 2;               /* 偏移2个字节 */
        g_iapbuf[i++] = temp;

        if (i == 1024)
        {
            i = 0;
            stmflash_write(fwaddr, g_iapbuf, 1024);
            fwaddr += 2048;     /* 偏移2048  16 = 2 * 8  所以要乘以2 */
        }
    }

    if (i)
    {
        stmflash_write(fwaddr, g_iapbuf, i);  /* 将最后的一些内容字节写进去 */
    }
}

/**
 * @brief       跳转到应用程序段(执行APP)
 * @param       appxaddr : 应用程序的起始地址

 * @retval      无
 */
void iap_load_app(uint32_t appxaddr)
{
    if (((*(volatile  uint32_t *)appxaddr) & 0x2FFE0000) == 0x20000000)     /* 检查栈顶地址是否合法.可以放在内部SRAM共64KB(0x20000000) */
    {
        /* 用户代码区第二个字为程序开始地址(复位地址) */
        jump2app = (iapfun) * (volatile uint32_t *)(appxaddr + 4);
        
        /* 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) */
        sys_msr_msp(*(volatile uint32_t *)appxaddr);
        
        /* 跳转到APP */
        jump2app();
    }
}

通过以上的函数,先检查程序的头两个位置的地址是否正确,正确才进行设置和 FLASH 的烧写;之后正式的运行 APP,就是检查一下地址是否合法,对的话调用 sys_msr_msp 然后跳转过去执行。

总结

这一个笔记是针对裸机开发可能会问的问题进行的一系列总结,之后还要总结 FreeRTOS,Linux 驱动和应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2082448.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 入门指南:Java IO流 —— 字符流

何为Java流 Java 中的流&#xff08;Stream&#xff09; 是用于在程序中读取或写入数据的抽象概念。流可以从不同的数据源&#xff08;输入流&#xff09;读取数据&#xff0c;也可以将数据写入到不同的目标&#xff08;输出流&#xff09;。流提供了一种统一的方式来处理不同…

【深入解析】最优控制中的Bellman方程——从决策到最优路径的探索

【深入解析】最优控制中的Bellman方程——从决策到最优路径的探索 关键词提炼 #Bellman方程 #最优控制 #动态规划 #值函数 #策略优化 #强化学习 第一节&#xff1a;Bellman方程的通俗解释与核心概念 1.1 通俗解释 Bellman方程是动态规划中的一个核心概念&#xff0c;它像是…

apache服务器的配置(服务名httpd,端口80 , 443)

目录 前言 配置文件 apache服务器的配置 安装apache服务器 配置防火墙 编辑配置文件 配置虚拟主机 基于域名的虚拟主机 配置dns服务器 将网站文件放到/var/www/目录下 修改主配置文件 新建vhost文件夹和xxx.conf文件 编辑 .conf 文件 检查配置 重启服务并访问网…

VS2022 QT环境显示中文乱码问题

1.问题描述 在VS2022中搭配QT6.2环境&#xff0c;在文本处设置中文&#xff0c;运行程序文本处显示乱码&#xff0c;未成功显示想要的中文。 2.VS2015解决方案 如果是VS2015的话&#xff0c;直接文件->高级保存选项可以设置编码格式。 修改编码格式如图所示&#xff1a;…

2024 Python3.10 系统入门+进阶(九):封装解构和集合Set常用操作详解

目录 一、封装和解构1.1 基本概念1.2 简单解构1.3 剩余变量解构1.4 嵌套解构1.5 其他解构1.6 序列模式匹配&#xff08;Python 3.10 最引人注目的新功能&#xff09;1.6.1 结构模式匹配的核心概念1.6.2 结构模式匹配的优势1.6.3 使用场景 二、集合Set2.1 初始化2.1.1 "{}&…

Java-数据结构-包装类和认识泛型 !!!∑(゚Д゚ノ)ノ

目录&#xff1a; 一、包装类&#xff1a; 1、基本数据类型所对应的包装类&#xff1a; 2、装箱和拆箱&#xff1a; 二、 泛型&#xff1a; 1、什么是泛型&#xff1a; 2、语法&#xff1a; 三、泛型类的使用&#xff1a; 四、裸类型&#xff1a; 五、泛型的擦除机制&…

82、k8s的service-NodePort端口开放和生命周期

0、单节点服务&#xff0c;以及k8s命令 [rootmaster01 ~]# kubectl create deployment nginx1 --imagenginx:1.22 --replicas3[rootmaster01 ~]# kubectl create deployment nginx1 --imagenginx:1.22 ##创建资源 deployment.apps/nginx1 created[rootmaster01 opt]# kubec…

软件设计原则之依赖倒置原则

依赖倒置原则&#xff08;Dependency Inversion Principle, DIP&#xff09;是软件设计中一个非常重要的原则&#xff0c;它属于面向对象设计的SOLID原则之一。这个原则的核心在于通过抽象来降低模块间的耦合度&#xff0c;使得系统更加灵活和可维护。 目录 依赖倒置原则的基本…

对标GPT-4o,科大讯飞正以大模型重塑语音产业

每个科技时代&#xff0c;都有每个时代的“入口”和“推手”。 在PC时代&#xff0c;浏览器和搜索引擎是主要入口&#xff0c;用户通过键盘和鼠标进行交互。移动互联时代&#xff0c;APP和应用商店成为典型入口&#xff0c;用户用手指和触摸屏进入互联网世界。而在眼下的AI时代…

8月27c++

提示并输入一个字符串&#xff0c;统计字符串中字母、数字、空格和其他字符的个数 代码 #include <iostream> #include <cstring> using namespace std;int main() {string str;cout<<"输入一个字符串";getline(cin,str);//输入字符串int lenstr…

【vulhub】Weblogic WLS Core Components 反序列化命令执行漏洞(CVE-2018-2628)

简单来说就是先用序列化工具ysoserial启动一个JRMP服务&#xff0c;加载先相关漏洞利用链&#xff0c;加载你要执行的恶意代码。 并将上述结果通过序列化工具ysoserial将我们的恶意代码进行一个序列化操作。 第二步就是将我们的exp去加载ysoserial序列化后的数据&#xff0c;后…

vue侧边栏

在Vue中创建一个侧边栏&#xff08;Sidebar&#xff09;是一个常见的需求&#xff0c;特别是在构建管理界面或需要导航菜单的应用时。侧边栏通常用于展示应用的导航链接或菜单项&#xff0c;用户可以通过点击这些链接来访问应用的不同部分。 <template><el-tree :data…

openEuler安装Docker和踩坑分析

我是用的openEuler版本&#xff1a;20.03 LTS SP1&#xff0c;安装在虚拟机中&#xff0c;使用ssh连接 在openEuler上安装Docker还是让我踩了不少坑&#xff0c;先看看这些坑是如何产生的 虽然官方没有说openEuler是基于什么开源系统开发的&#xff0c;但大致内容和CentOS相似…

【教学新纪元】大学电工电子课堂大变身!SmartEDA电路仿真软件助你高效授课✨

在快速迭代的科技时代&#xff0c;教学方式的创新成为了提升教学质量的关键。作为一位深耕电工电子原理教学领域的老师&#xff0c;你是否曾梦想过将复杂的电路理论以直观、互动的方式展现给学生&#xff1f;今天&#xff0c;就让我们一起探索如何利用SmartEDA电路仿真软件&…

鸿蒙开发5.0【基于lycium的开源c库编译与集成】

场景描述 对于c库编译问题&#xff0c;应用经常会遇到如下业务诉求 场景一&#xff1a;基于HarmonyOS编译开源C库 场景二&#xff1a;开源c库编译完成后的集成 方案描述 场景一&#xff1a; 需要使用开源c库 lycium的使用说明&#xff1a;lycium的特点就是自动化编译&am…

8.28-回顾+容器与主机之间的通信+跨主机容器之间的通信

一、回顾 1.启动docker systemctl start docker 2.拉取registry docker pull registry 3.启动镜像&#xff0c;同时挂载目录&#xff08;保存镜像&#xff09;端口映射5000 docker run -d -v /regist/:/var/lib/registry/ -p5000:5000 registry:latest 4.修改/etc/docker/d…

培训第三十八天(上传镜像,私有仓库下载镜像,跨主机容器间的通信,harbor软件包下载)

1、harbor软件包下载 https://github.com/search?qharbor&typerepositories 2、出现拒绝连接错误&#xff0c;可能是由于容器没开 # 问题解决&#xff1a;[rootdocker ~]# curl localhost:5000/v2/_catalogcurl: (7) Failed connect to localhost:5000; 拒绝连接[rootdoc…

pdf怎么转换成excel?掌握好这9个pdf转换方法就够了(全)

pdf怎么转换成excel&#xff1f;日常的办公生活中&#xff0c;我们经常需要接触很多文档格式&#xff0c;而pdf格式文件因为其稳定性和安全性受到很多办公人士的喜爱。但PDF文件不能直接编辑&#xff0c;很多小伙伴们就会出现关于pdf格式转换的难题&#xff0c;比如说想把一份带…

MATLAB虫害检测预警系统

一、课题介绍 本课题是基于MATLAB颜色的植物虫害检测识别&#xff0c;可以辨析植物叶子属于是轻度虫害&#xff0c;中度虫害&#xff0c;严重虫害&#xff0c;正常等四个级别。算法流程&#xff1a;每种等级叶子分别放在同一个文件夹&#xff0c;训练得到每个文件夹每个叶…

SSL安全认证网关:保障网络安全的强大护盾

随着信息技术的飞速发展&#xff0c;我们的生活和工作越来越依赖于网络&#xff0c;但与此同时&#xff0c;网络安全威胁也日益严峻。为了保护我们的信息安全&#xff0c;各种安全技术和产品应运而生&#xff0c;其中SSL安全认证网关就是一种非常重要的安全防护工具。 今天&…