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

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

相关基础知识

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);
#endifOLED_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_IN0PA1对应 ADC1_IN1PA2对应 ADC1_IN2PA3对应 ADC1_IN3PA4对应 ADC1_IN4PA5对应 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 */
#endifCAN_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.mzph.cn/bicheng/52681.shtml

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

相关文章

目标 CDC实例数据库更改密码,预定启动报错SQL 错误代码为“-30082”。SQL 状态为:08001。

更改完CDC目标端实例密码后&#xff0c;登录MC更新存储器密码&#xff0c;存储器可正常连接&#xff0c;启动预定报错如下&#xff1a; 源 IBM Data Replication 未获授权&#xff0c;无法复制到该目标。 登录认证失败。 发生 SQL 异常。SQL 错误代码为“-30082”。SQL 状态…

嵌入式学习——ARM学习(1)

1、存储器 高速缓存&#xff08;Cache&#xff09;通常分为三级&#xff1a;L1、L2 和 L3。它们的主要功能和特点如下&#xff1a; 这三级缓存的设计旨在通过层次化存储来优化数据访问速度和处理器性能。 1、L1 缓存&#xff1a; 位置&#xff1a;直接集成在处理器核心内。 大小…

打手机检测算法源码样本展示打手机检测算法实际应用场景介绍

打手机检测算法是一种利用计算机视觉技术来监测和识别人们在特定区域如驾驶舱、考场或其他敏感区域非法使用手机的行为。这种算法对于提高安全性和确保规则的遵守具有重要意义。以下是关于打手机检测算法源码及其实际应用的详细阐述&#xff1a; 1. 算法实现 - 深度学习框架&a…

【selenium点选下拉框】解决无法选中对应选项的问题

需求 使用selenium点击下拉框&#xff0c;选中【是】选项。 代码 方法1 # 点击下拉框 driver.find_element(xpath,//*[id"basicProcessDetail"]/div[2]/div[2]/div[1]/div/div[2]/div/div/div/div[1]/div[2]/form/div[11]/div[1]/div/div/div[1]/div[1]/div/i).…

雷达图概述以及实例

目录 一.雷达图概述1.何为雷达图2.雷达图的构成要素 二.实例&#xff08;以Excel、Python为例 &#xff09;1.Excel&#xff08;2021版&#xff09;2.Python 一.雷达图概述 1.何为雷达图 雷达图&#xff0c;是一种展现多维度数据间相对重要性或程度的可视化图形。以中心点为起…

LabVIEW高速数据采集关键问题

在LabVIEW进行高速数据采集时&#xff0c;需要关注以下几个关键问题&#xff1a; 数据采集硬件的选择: 高速数据采集需要高性能的数据采集硬件&#xff0c;例如NI PXIe、USB DAQ等模块。硬件的选择应根据采集速率、通道数、精度、以及应用场景的具体需求来确定。 采集速率与带…

ByteBuffer详解

文章目录 1. ByteBuffer是抽象类&#xff0c;他的主要实现类为2. 获取方式3. 核心结构4. 核心API5. 字符串操作 1. ByteBuffer是抽象类&#xff0c;他的主要实现类为 HeapByteBuffer 堆ByteBuffer JVM内的堆内存 —> 读写操作 效率低 会受到GC影响MappedByteBuffer(DirectB…

桥接模式-多类型登录方式的思考

桥接模式-SSO单点登录 背景:(业务细节已脱敏)需求:问题:解决方式: OAuth2.0 实现单点登录四种授权模式桥接模式优化问题代码实现 背景:(业务细节已脱敏) 基于实习期间的一个代码重构的思考——业务细节已脱敏 基于内部旧框架实现业务toB管理系统&#xff0c;需要迁移数据并新的…

uni-app 手记集。

1、uni-app 是一个使用 Vue.js 开发的前端应用的框架&#xff0c;所以不会Vue.js的小伙伴可以先去看看Vue.js的基础教学。 2、.vue文件结构 <template><div class"container"></div> </template><script type"text/ecmascript-6&q…

【DiskGenius硬盘分区】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

TCP系列相关内容

一、TCP上传文件 loop——本地回环测试地址。 void *memset&#xff08;void *s,int c,size_t n&#xff09;——给一个变量设定一个值。 1、“粘包”问题 两次分别发送的数据&#xff0c;被一起接收形成该现象。 原因&#xff1a;TCP流式套接字&#xff0c;数据与数据间没…

前端速通面经八股系列(二)—— HTML篇

HTML高频面经八股目录 1. src和href的区别2. 对HTML语义化的理解3. DOCTYPE(⽂档类型) 的作⽤4. script标签中defer和async的区别5. 常⽤的meta标签有哪些6. HTML5有哪些更新1. 语义化标签2. 媒体标签3. 表单4. 进度条、度量器5.DOM查询操作6. Web存储7. 其他 7. img的srcset属…

三种通过代码创建矢量文件的方法及例子

现有四个点&#xff1a;(1, 1), (2, 2), (3, 3), (4, 4) 以这四个点围起来就是一个面。 如何通过python创建矢量文件。 我们以创建一个面矢量文件为例子&#xff0c;进行阐释。 我们可以使用geopandas、fiona、gdal库完成矢量创建。 geopandas 假设我们创建的矢量文件格式…

Chrome H265 WebRTC 支持

Chrome从127版本开始支持RTC H265解码&#xff0c;这样服务器就不需要对H265转码了&#xff0c; H5S和USC会自动检测浏览器支持的解码类型并自动判断是否启动转码&#xff0c;这样客户端不用关心摄像机具体是H264还是H265&#xff0c;尽量使用带GPU的客户端&#xff0c;这样服务…

ArcGIS应用指南:近邻分析(点匹配到最近线段上)

近邻分析通常用于确定一个要素集中的要素与另一个要素集中最近要素的距离。当涉及到点匹配到最近的线时&#xff0c;这种分析可以用来确定每个点到最近线段的距离及位置&#xff0c;也就是我们常说的点匹配到最近线上&#xff0c;可以参考官方文档&#xff1a;近邻分析 (Covera…

动态规划之买卖股票篇-代码随想录算法训练营第三十八天| 买卖股票的最佳时机ⅠⅡⅢⅣ,309.最佳买卖股票时机含冷冻期,714.买卖股票的最佳时机含手续费

121. 买卖股票的最佳时机 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 讲解视频&#xff1a; 动态规划之 LeetCode&#xff1a;121.买卖股票的最佳时机1 题目描述&#xff1a; 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定…

软件测试-Selenium+python自动化测试

目录 一、元素定位 1.1一个简单的模板 1.2单选框radio定位实战 1.3下拉操作 1.4弹窗 1.5文件上传 1.6 iframe(类似于页中页,嵌套进去了) 二、元素定位实战 会用到谷歌浏览器Chrome测试,需要下载一个Chromedriver(Chrome for Testing availability)对应自己的浏览…

华为2024 届秋招招聘——硬件技术工程师-电源方向-机试题(四套)(每套四十题)

华为 2024 届秋招——硬件-电源机试题&#xff08;四套&#xff09;&#xff08;每套四十题&#xff09; 岗位——硬件技术工程师 岗位意向——电源 真题题目分享&#xff0c;完整版带答案(有答案和解析&#xff0c;答案非官方&#xff0c;未仔细校正&#xff0c;仅供参考&am…

OpenCV杂项图像变换(1)自适应阈值处理函数adaptiveThreshold()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 函数对数组应用自适应阈值。 该函数根据以下公式将灰度图像转换为二值图像&#xff1a; 对于 THRESH_BINARY: t e x t d s t ( x , y ) { maxV…

OpenAI API: How to count tokens before API request

题意&#xff1a;“OpenAI API&#xff1a;如何在 API 请求之前计算令牌数量” 问题背景&#xff1a; I would like to count the tokens of my OpenAI API request in R before sending it (version gpt-3.5-turbo). Since the OpenAI API has rate limits, this seems impor…