底层相关的内容,之前掌握的不扎实,现在重新把相关重点记录一下,做个笔记记诵。
相关基础知识
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 的 DCode 接到内部 FLASH;System 接到了内部 SRAM;DMA1 去到 FSMC;DMA2 去到所有的 APB 设备。
- IBus 指令总线,连到 FLASH,获取指令;
- DBus,数据总线,连到 SRAM、FLASH 等,访问数据;
- SBus,连接到所有外设;
- DMA 总线,实现数据自动搬运;
- 内部 FLASH,硬盘,代码/数据存储,最高 24MHz,所以因为 72MHz的主频率,插入 2 个时钟周期延迟;
- 内部 SRAM,内存,数据存储,最快 72MHz;
- FSMC,灵活静态存储控制器,就是外部总线接口,可以访问 SRAM、NAND FLASH、NOR FLASH 等;
- AHB/APB 桥,AHB 最高 72MHz,APB2 一样,但是 APB1 最高是 36MHz;
- 总线矩阵,访问仲裁用。
存储器需要映射,ST 是将 4GB(32位芯片)分为 8 个块,如下:
主要就是前三个块,直接截图稍微记一记:
如果要直接操作寄存器,那就去找地址,寄存器地址 = BUS_BASE_ADDR + PERIPH_OFFSET + REG_OFFSET(总线基地址 + 外设基于总线基地址的偏移量 + 寄存器相对外设基地址的偏移量)。寄存器映射都在 stm32f103xw.h 中。
M4 内核
一样,贴一个电机开发板的:
是类似的,这里写一点区别:
- 内部 FLASH,168MHz的主频,所以要插入 8 个时钟周期延迟;
- 内部 SRAM,可以实现最高 168MHz;
- AHB 和 APB2 最高 84MHz,APB1 只有 42MHz。
其余都是差不多的。
启动过程详解
三种复位:上电复位、硬件复位、软件复位。复位之后两件事:
- 从地址 0x0000 0000 处取出堆栈指针 MSP 的初始值,该值就是栈顶地址。
- 从地址 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 来升级。
启动做的事情:
- 初始化堆栈指针 SP = _initial_sp
- 初始化程序计数器指针 PC = Reset_Handler
- 设置堆和栈的大小
- 初始化中断向量表
- 配置外部 SRAM作为数据存储器(可选)
- 配置系统时钟,通过调用 SystemInit函数(可选)
- 调用 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 种功能模式。具体的电路图不记了,应该不会那么细致。
- 输入浮空:上下拉断开,施密特触发器打开,输出禁止。IO 电平完全由外部电路决定,用于按键检测等。
- 输入上拉:上拉电阻导通其余一致。
- 输入下拉:下拉电阻导通其余一致。
- 模拟功能:上下拉断开,施密特触发器关闭,双 MOS 关闭**。用于 ADC、DAC等,也有休眠省电的配置。**
- 开漏输出:只能输出低电平 Vss 或者高阻态,P-MOS 一直截止,不导通,相当于一直 VDD,常用于 IIC(IIC_SDA)等。开漏输出模式下,可以读取 IO 引脚状态 。
- 推挽输出:输出低电平 Vss 或者高电平 VDD。推挽输出跟开漏输出不同的是,推挽输出模式 P-MOS 管和 N-MOS 管都用上。同样可以读取 IO 电平。
- 开漏复用:IO 作为其他外设的特殊功能引脚。状态由相应外设控制,而不是输出寄存器。其余就是开漏输出。
- 推挽复用:复用和推挽两个结合。
像使用 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 输出,一共有四种情况:
这个还是要记一下,关键的八股文。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 个外部。
转换顺序,分为规则组和注入组,可以等效认为是常规的和中断的,注入组可以打断规则组,注入组执行完了才执行原先的规则组。
触发的话可以分为两种,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 是半双工通信。
时序图如下:
需要注意的是,SCL 和 SDA 都是高电平,那就是 IIC 的空闲状态。起始信号和停止信号比较简单;应答信号,就是脉冲 9 期间低电平为 ACK,高电平为 NACK,表示接收器接受该字节没成功。
写操作如下:
从图中就可以看到,需要先发送的是 0 的写操作的从机地址(也就是从机地址 + 0),然后收到 ACK 之后就可以发送数据了。
读操作如下:
这里一开始发送的就是 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_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 协议有 5 个帧类型:数据帧、遥控帧、错误帧、过载帧、间隔帧。数据帧以及遥控帧又分为标准格式和扩展格式。
一般就是用数据帧来完成数据传输。数据帧有 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,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)。
实际使用,初始化需要 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 的两种不同运行流程:
需要注意,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 驱动和应用。