STM32CubeMX入门
前言
cubemx是一个可以自动生成代码的单片机学习和使用的软件,非常方便,但同时配置时需要注意芯片以及外设的各个引脚的配置以及使用情况
下载
直接在官网上下载
基础配置
需要完成的如下
- 新建工程
- 在右侧查找自己所使用的芯片型号,然后选择对应的芯片
- 打开 RCC,并且设置为外部时钟源
- 然后配置时钟树
一个需要注意的问题
在生成代码之后,在第一次编译时会有一个报错,就是找不到对应的启动文件,也就是 startup
开头的汇编文件( startup_stm32f407xx.s
),是用来启动整个项目的,这个文件可以在 CubeMX 保存在 C 盘中的系统文件中找到
开启调试
在 SYS 中的第一个选项 Debug
中选择 Serial Wire
即可开启 SWD 的调试,如下
时钟树的配置
时钟介绍
时钟是单片机运行的基础,时钟信号推动单片机内各个部分执行相应的指令。时钟系统就是CPU的脉搏,决定cpu速率,像人的心跳一样 只有有了心跳,人才能做其他的事情,而单片机有了时钟,才能够运行执行指令,才能够做其他的处理 (点灯,串口,ADC),时钟的重要性不言而喻。
STM32本身十分复杂,外设非常多 但我们实际使用的时候只会用到有限的几个外设,使用任何外设都需要时钟才能启动,但并不是所有的外设都需要系统时钟那么高的频率,为了兼容不同速度的设备,有些高速,有些低速,如果都用高速时钟,势必造成浪费。并且,同一个电路,时钟越快功耗越快,同时抗电磁干扰能力也就越弱,所以较为复杂的MCU都是采用多时钟源的方法来解决这些问题。所以便有了STM32的时钟系统和时钟树
时钟系统的作用
- STM32 时钟系统主要目的时给相对独立的外设模块提供时钟,也是为了降低整个芯片的耗能。
- 系统时钟,是处理器运行时间基准(每一条机器指令一个时钟周期)
- 时钟是单片机运行的基础,时钟信号推动单片机内各个部分执行相应的指令
- 一个单片机内部提供多个不同的时钟,可以适用于更多场景
- 不同的功能模块会有不同的时钟上限,因此提供不同的时钟,也能在一个单片机内放置更多的功能模块。对不同模块的时钟增加开启和关闭功能,可以降低单片机的功耗
- STM32 为了低功耗,他将所有的外设时钟都设置为 disable(不使能),用到什么外设,只要打开对应外设的时钟就可以, 其他的没用到的可以还是 disable(不使能),这样耗能就会减少
系统时钟树图示
四个时钟源和锁相环倍频输出时钟
图中可以看出 stm32 有四个时钟源和锁相环倍频输出时钟
- HSE:高速外部时钟,高速外部时钟信号有两个时钟源
- HSE 外部晶振/陶瓷谐振器
- HSE 外部用户时钟
- HSI:高速内部时钟,HSI 高速时钟信号由内部 16 MHz RC 振荡器生成,可以直接作为系统时钟或者用作 PLL 输入
- LSE:低速外部时钟,LSE 晶振是 32.768 kHz 低速外部晶振或者陶瓷谐振器,可作为实时时钟外设 RTC 的时钟源来提供时钟/日历或者其它定时功能,具有低功耗且精度高的优点
- LSI:低速内部时钟,LSI RC 可以作为低功耗时钟源在停机和待机模式下保持运行,供独立看门狗(IWDG)和自动唤醒单元(AWU)使用。时钟频率在 32 kHz 左右
- PLL:倍频输出时钟
- 主 PLL 由 HSE 或者 HSI 提供的时钟信号(通过选择器)。主 PLL 时钟计算方式为: $PLL=12MHz*N/(MP)=168MHz$ ,在其中外部晶振选择 12 MHz ,M=6,分频器分频系数 P=2,倍频器倍频系数 N=168,并且具有两个不同的输出时钟。
- 第一个输出 PLLP 用于生成高速的系统时钟(最高 168 MHz)
- 第二个输出 PLLQ 用于生成 USB OTG FS 的时钟(48 MHz),随机数发生器的时钟和 SDIO 时钟
- 专用 PLL(PLLI2S)用于生成精确的时钟,从而在 I2S 接口实现高品质音频性能
- 主 PLL 由 HSE 或者 HSI 提供的时钟信号(通过选择器)。主 PLL 时钟计算方式为: $PLL=12MHz*N/(MP)=168MHz$ ,在其中外部晶振选择 12 MHz ,M=6,分频器分频系数 P=2,倍频器倍频系数 N=168,并且具有两个不同的输出时钟。
锁相环原理
锁相环(Phase-Locked Loop,PLL)是一种电子电路,主要用于频率锁定,即当输入信号的频率发生变化时,它能够自动调整自身产生的信号频率,使其保持与输入信号同步。PLL 通常由以下几个部分组成:
- 鉴相器(Phase Comparator):专用于提供一种频率会在合理的范围内随输入信号的电压幅值变化而变化的输出信号。将输入信号与参考信号比较,产生一个误差电压,这个电压反映了两个信号之间的相位差。
- 电压控制放大器(VCO,Voltage-Controlled Oscillator):根据鉴相器输出的误差电压调整自身的输出频率。当误差减小时,VCO会增加输出频率;反之,当误差增大时,VCO会减少输出频率。
- 低通滤波器(Loop Filter):用于平滑并限制鉴相器输出的变化,帮助 PLL 达到稳定的锁定状态。
- 电荷放大器:有点像电路里的放大器
通过这种反馈机制,锁相环可以在宽频范围内跟踪和稳定频率,常被应用在通信系统、定时基准、雷达测距以及数字信号处理等领域。
基本应用
- 时钟恢复
- 偏移校正:在信号的传输过程中,工艺、温度、电压会影响时钟沿与数据采样窗口的延时,这一延时限制了数据发送的频率。可以在数据的接收端使用偏移校正 PLL 来消除这个延时,这样每一个采样触发器的时钟信号都与接收时钟保持相位匹配
- 产生时钟:当今大多数电子系统中都包含有不同种类的处理器。典型情况下,外部为处理器提供一个较低的时钟频率,然后在处理器中使用PLL将其倍频或分频到处理器需要的时钟频率
MCO 两个向外输出的时钟口
图中还可以看到两个向外输出的时钟口如下
- STM32 时钟信号输出 MCO1(PA8):MCO1 用户可以通过可配置的预分频器(从 1 到 5)向 MCO1 引脚输出四个不同的时钟源
- HSI 时钟
- LSE 时钟
- HSE 时钟
- PLL 时钟
- STM32 时钟信号输出 MCO2(PC9):MCO2 用户可以通过可配置的预分频器(从 1 到 5)向 MCO2 引脚输出四个不同的时钟源
- HSE 时钟
- PLL 时钟
- 系统时钟
- PLLI2S 时钟
一般来说这两个向外输出的时钟口是不工作的,就是灰色,只要在 RCC 设置中打开 Master Clock Output 即可启用。然后即可引出时钟输出
FCLK 自由运行时钟
自由运行时钟。如果为了节省电量,STM32 可以进入低功耗模式中的停止模式时,AHB 总线会停止运行,同时 HCLK 会停止传输时钟脉冲从而所有连接到 HCLK 的外设都会停止工作。此时要想重新唤醒 STM32 就需要依赖外部中断了,此时 FCLK 的作用就显现出来了,它是连接在 AHB 的预分频器之下的,并不是位于 HCLK 下,所以当 HCLK 停止运作之后,FCLK 还是会继续进行工作的,为中断采样提供时钟信号
CSS 时钟安全系统
时钟安全系统的作用就是当外部高速时钟出错时,会将时钟源切换到高速内部时钟,并且可以产生中断,从而可以进行紧急制动等紧急处理
RTC 和 IWDG 时钟线
- RTC:这一路时钟线的时钟源只能是低速内部时钟,低速外部时钟和外部时钟的 128 分频
- IWDG:由内部低速时钟提供信号
PTP 时钟
这是一个通过网络同步的时钟的一个协议,当硬件支持时,PTP 精度高达亚微秒
时钟配置
- 对于 HSI,HSE,PLL 等时钟源的配置,没有专门的固件库函数,可以通过
SystemInit
函数来操作配置,该函数具体实现如下- 系统复位之后,先调用
SystemInit
函数,该函数会初始化系统时钟,设置 PLL - 打开 HSE,等待其稳定
- 设置 AHB,APBx 等分频系数
- 设置 HSE 为主的 PLL 时钟源,并且配置主 PLL 里的分频和倍频系数,然后产生 PLLCLK 并将其使能,并且选择系统时钟(SYSCLK)为 PLLCLK
- 系统复位之后,先调用
初始化之后
- SYSCLK 系统时钟 = 168 MHz
- AHB 总线时钟(HCLK=SYSCLK)= 168 MHz
- APB1 总线时钟(PCLK1=SYSCLK/4)= 42 MHz
- APB2 总线时钟(PCLK2=SYSCLK/2)= 84 MHz
PLL 主时钟 = 168 MHz
初始化之后可以通过变量
SystemCoreClock
获取系统变量,如果 SYSCLK=168MHz,那么变量等于 168000000系统复位之后会先调用
SystemInit
函数,其次才是main
函数,这一点在启动文件中有写1
2
3
4
5
6
7
8
9...
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
...
SysTick 系统定时器使用方法
SysTick
为定时器寄存器,24 位,只能递减,该寄存器存在于内核,嵌套在 NVIC 中,所有的 Cortex-M 内核单片机都具有该定时器。 SysTick(uint32_t ticks)
初始化函数存在于 Core_m4.h
中,计数器每计数一次的时间位 1/SysTick
,一般会设置 SysTick=168MHz
还有一些与 SysTick
相关的寄存器(位于 core_cm4.h 中)
1 | typedef struct |
CTRL
SysTick
控制以及状态寄存器LOAD
SysTick
重装载数值寄存器,保存着SysTick
计数到 0 时重新装载到SysTick
的值VAL
SysTick
当前数值寄存器CALIB
SysTick
校准数值寄存器COUNTFLAG
16 位寄存器,如果在上次读取本寄存器之后,SysTick
已经计数到 0,则该位置位CLKSOURCE
2 位,时钟源选择位- 0:AHB / 8
- 1:处理器时钟 AHB
TICKINT
1 位- 1:
SysTick
倒数计数到 0 时产生SysTick
一场请求 - 0:数到 0 时无动作
- 1:
ENABLE
SysTick
定时器的使能位RELOAD
当SysTick
计数到 0 时,将被重新载入到SysTick
的值
设置 SysTick
定时器,只需要调用 SysTick_Config(uint32 ticks)
函数,通过函数向 SysTick
中写入值,如果时钟源选择的是 AHB=168MHz
,那每递减一次的时间就是 1/168M,需要多少时间就设定多少初始值。当减到零时会触发异常请求中断。该函数具体实现如下
1 | __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks) |
系统时钟通过 AHB 分频器给外设提供时钟
上述从左到右可以为:系统时钟——AHB 分频器——外设分频/倍频器——外设时钟设置
系统时钟的 SYSCLK
通过 AHB 分频器分频之后送给各模块使用,AHB 分频器可以选择 1,2,4,8,…,512 分频,其中 AHB 的分频器输出的时钟给 5 个模块使用
- 内核总线 AHB:送给 AHB 总线,内核,内存和 DMA使用的 HCLK 时钟
Tick
定时器:通过 8 分频之后送给 Cortex 的系统定时器时钟- I2S 总线:直接送给 Cortex 的空闲运行时钟 FCLK
- APB1 外设:送给 APB1 分频器。APB1 可以选择 1,2,4,8,16 分频,其输出一路供 APB1 外设使用(PCLK1 最大频率为 42MHz),另一路送给通用定时器使用。该倍频器可以选择 1 或者 2 倍频,时钟输出供定时器 2-7 使用
- APB2 外设:送给 APB2 分频器。APB2 分频器可选择 1,2,4,8,16 分频,其输出一路供 APB2 外设使用(PCLK2 最大频率 84MHz),另外一路送给高级定时器。该倍频器可以选择 1 或者 2 倍频,时钟输出供定时器 1 和 定时器 8 使用。另外还有一路输出供 ADC 分频器使用,分频后送给 ADC 模块使用。ADC 分频器可以选择 2,4,6,8 分频。
需要注意的是:如果 APB 预分频器的分频系数为 1,则定时器时钟频率(TIMxCLK)为 PCLKx。否则定时器频率将为 APB 的频率的两倍 $TIMxCLK = 2\times PCLKx$
APB 对应的外设
APB1 上连接的是低速外设,包括:电源接口,CAN,USB,I2C1,I2C2,USART2,USART3,UART4,UART5,SPI2,SPI3,通用定时器 2-5 和 12-14,基本定时器 6-7
APB2 上连接的是高速外设,包括:USART1,UASRT6,SPI1,ADC1,ADC2,ADC3,所有的普通 IO 口,第二功能的 IO 口,高级定时器 1 和 8,通用定时器 9-11
RCC初始化
首先是 RCC 寄存器
1 | typedef struct |
相应的 RCC 初始化的流程如下
- 将RCC寄存器重新设置为默认值
- 打开外部高速时钟晶振HSE
- 等待外部高速时钟晶振工作
- 设置AHB时钟
- 设置高速AHB时钟
- 设置低速速AHB时钟
- 设置PLL
- 打开PLL
- 等待PLL工作
- 设置系统时钟
- 判断是否PLL是系统时钟
- 打开要使用的外设时钟
RCC 初始化就是系统时钟初始化,这个在 CubeMX 生成的代码中可以看到,也就是如下函数 ,这个函数的内部实现就是在 CubeMX 中配置的时钟树框图的配置
1 | void SystemClock_Config(void) |
RCC 配置
RCC 配置只需要选择 High Speed Clock
,这里将其设置为 Crystal/Ceramic Resonator
也就是晶体/陶瓷晶振,另外一个选项是 BYPASS Clock Source
(旁路时钟)
时钟监视系统
STM32 还提供了一个时钟监视系统(CSS),用于监视高速外部时钟(HSE)的工作状态。倘若HSE失效,会自动切换(高速内部时钟)HSI作为系统时钟的输入,保证系统的正常运行。
LED 配置
原理
LED 即发光二极管。当电子与空穴复合时能辐射出可见光,因而可以用来制成发光二极管。LED是正向导通,反向截止的,它在电路设计中的符号如下图所示
其中
- 1 端接电源正极
- 2 端接电源负极
配置
在 CubeMX 中配置 LED 时,首先需要先看板子的手册或者 PCB 或原理图,找到连接 LED 的引脚,这里使用的是大疆的开发板 C 板,所以对应的 LED 灯的引脚就是 PH10-PH12,所以这里就会将该引脚的功能设置为输出模式,配置如下
GPIO output level
初始化完成之后输出的电平状态为低GPIO mode
设置输出模式为推挽输出GPIO Pull-up/Pull-down
设置上拉或者下拉模式,这里设置为不上拉也不下拉Maximum output speed
最大输出速度,这里设置为低User Label
用户标签,这里没有设置,相当于是把引脚起了个别名
并且设置为推挽输出模式,输出速度设置为慢就可以,然后就完成设置,就可以点击右上方的 GENERATE CODE 生成代码了
可以通过如下的指令来控制 LED 灯的亮灭
1 | // 写入引脚 |
呼吸灯
由于 LED 的引脚为 PH10~PH12
,而且对应着 TIM5_CH1~TIM5_CH3
。设置预分频为 42-1
,重载计数值为 1000-1
代码实现
1 | void argb_led_show(uint32_t argb) { |
蜂鸣器配置
介绍
蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电蜂鸣器。主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。有源蜂鸣器自带了震荡电路,一通电就会发声,无源蜂鸣器则没有自带震荡电路,必须外部提供 2~5Khz 左右的方波驱动才能发声。
配置
这里选择引脚为 PD14,这里设置为
GPIO output level
初始化完成之后输出的电平状态为低GPIO mode
设置输出模式为推挽输出GPIO Pull-up/Pull-down
设置上拉或者下拉模式,这里设置为下拉Maximum output speed
最大输出速度,这里设置为高速User Label
用户标签,这里没有设置,相当于是把引脚起了个别名
然后即可点击生成代码,这里使用如下指令控制蜂鸣器
1 | void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) |
但是上述对蜂鸣器的配置是一般蜂鸣器的配置,对于大疆 C 板,蜂鸣器是需要 PWM 来控制的,所以上述生成的代码并不能使蜂鸣器发出响声
定时器配置
介绍
在 STM32F4 系列中共有 15 个定时器
- 基本定时器:TIM6,TIM7
- 通用定时器:TIM2~TIM5,TIM9~TIM14
- 高级定时器:TIM1,TIM8
其中,各自功能如下
- 基本定时器
- 16位向上、向下、向上/下自动装载计数器
- 16位可编((可以实时修改)预分频器,计数器时钟频率的分频系数为1~65535之间的任意数值
- 位于APB1总线上
- 单纯的定时计数器
- 通用定时器
- 16位向上、向下、向上/下自动装载计数器
- 16位可编((可以实时修改)预分频器,计数器时钟频率的分频系数为1~65535之间的任意数值
- 四个独立通道(TIMx_CH1~TIMx_CH4),可用作
- 测量输入信号的脉冲长度(输入捕获)
- 输出比较
- 单脉冲模式输出
- PWM 输出(边缘或者中间对齐模式)
- 支持增量编码器和霍尔传感器电路
- 产生中断的原因
- 更新:计数器向上/向下溢出,计数器初始化(通过软件或者内部/外部触发)
- 触发事件(计数器启动,停止,初始化或者由内部/外部触发计数)
- 输入捕获
- 输出比较
- 位于 APB1 总线上
- 高级定时器
- 具有基本定时器和通用定时器的所有功能
- 能控制交,直流电机所有功能
- 输出 6 路互补带死区的信号,刹车功能等
- 位于 APB2 总线上
定时器计数模式
- 向上计数模式:计数器从 0 开始计数到自动加载值(
TIMx_ARR
),然后重新从 0 开始计数并且产生一个计数器溢出事件 - 向下计数模式:计数器从自动装入的值(
TIMx_ARR
)开始计数直到 0,然后从自动装入的值重新开始,并且产生一个计数器向下溢出的事件 - 中央对齐模式(向上/向下计数):计数器从 0 开始计数到自动装填的值 - 1,产生一个计数器溢出事件,然后向下计数到 1 并且产生一个计数器溢出事件,不断循环
计数时钟的选择
计数器时钟可由下列时钟源提供
- 内部时钟(TIMx_CLK)
- 外部时钟模式1:外部捕捉比较引脚(TIMx)
- 外部时钟模式2:外部引脚输入(TIMx_ETR),仅适用于 TIM2~TIM4
- 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器
配置
这里选择 TIM2,设置时钟源为内部时钟,定时器的频率计算公式为 $f=\frac{f_1}{(PSC+1)(ARR+1)}$
这里将定时器设置为 1ms,并且由于该定时器是接入在 APB1 总线上的,所以 $f_1=42MHz$ ,同时选择预分频系数 $PSC=42-1$ ,选择自动重载数 $ARR=1000-1$ ,定时器频率为 $f=1000$ ,也就是 1ms
然后打开定时器中断,如下
使能 TIM2 全局中断,然后生成代码,这里需要注意,生成代码之后需要在程序中做一些修改才能使用
在代码中查找,可以看到在定时器中断函数 TIM2_IRQHandler
(位于 stm32f4xx_it.c
中)中调用了 HAL_TIM_IRQHandler
(位于 stm32f4xx_hal_tim.c
文件中)函数,用来处理定时器中断,而这些函数都存在于标准库或者霍尔库中,并且查看 HAL_TIM_IRQHandler
函数时,发现该函数会调用 HAL_TIM_PeriodElapsedCallback
函数,而这个函数在下面定义了一个虚函数版本。所以需要我们自定义该函数,所以在 main.c
文件中创建定义该函数,如下
1 | void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { |
做完这些之后,实际上代码依旧不能进入中断函数,所以这里就需要在 main
函数中将定时器计数中断打开,也就是如下
1 | MX_TIM2_Init(); // TIM2 初始化 |
然后编译之后就能成功运行了
PWM 配置
简介
脉冲宽度调制,是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。
原理
STM32F4 共有 15 个定时器:高级定时器(TIM1、TIM8);通用定时器(TIM2、TIM3、TIM4、TIM5、TIM9~TIM14);基本定时器(TIM6、TIM7)
而且 STM32 的每个通用定时器都有独立的 4 个通道可以用来作为:输入捕捉,输出比较,PWM 输出,单脉冲输出模式等。STM32 的定时器除了 TIM6 和 TIM7 之外,其它的定时器都可以产生 PWM 输出。其中高级定时器可以产生 7 路的 PWM 输出
下面是向上计数模式
在 PWM 输出模式下,除了 CNT(计数器当前值),ARR(自动重装载值)之外,还多了一个值就是 CRRx(捕获/比较寄存器值)。当 CNT 小于 CRRx 时,TIMx_CHx 通道输出低电平,否则输出高电平
具体工作流程
- 定时器从 0 开始向上计数
- 当 0~t1 段,定时器计数器 TIMx_CNT 值小于 CCRx 值,输出低电平
- 在 t1~t2 段时,定时器计数器 TIMx_CNT 值大于 CCRx 值,输出高电平
- 当 TIMx_CNT 值达到 ARR 时,定时器溢出,重新开始向上计数
原理总结
- 每个定时器有四个通道,每一个通道都有一个捕获比较寄存器
- 将寄存器值和计数器值比较,通过比较结果输出高低电平,便可以实现脉冲宽度调制模式(PWM 信号),也就是在定时器开始之后,计数值也就是
CNT
累增,一直到计数值之后就会重新开始计数,在这个过程中CNT
一直增加,输出也与CRRx
比较来输出从而实现在一个周期之内,输出 TIMx_ARR
寄存器确定 PWM 频率TIMx_CCRx
寄存器确定占空比
PWM 工作模式
- PWM 模式1
- 在向上计数时,一旦
TIMx_CNT<TIMx_CCR1
时通道 1 为有效电平(OC1REF=1
),否则为无效电平(OC1REF=0
) - 在向下计数时,一旦
TIMx_CNT>TIMx_CCR1
时通道 1 为无效电平,否则为有效电平
- 在向上计数时,一旦
- PWM 模式2
- 在向上计数时,一旦
TIMx_CNT<TIMx_CCR1
时通道 1 为无效电平,否则为有效电平 - 在向下计数时,一旦
TIMx_CNT>TIMx_CCR1
时通道 1 为有效电平,否则为无效电平。
- 在向上计数时,一旦
在两种模式下 TIMx_CNT
(计数器当前值)与 TIMx_CCR1
(捕获/比较值)只是决定是有效电平还是无效电平
有效电平可以是高电平也可以是低电平,这需要结合 CCER
寄存器的 CC1P
位的值来确定。如果为向上计数,且 CCER
寄存器的 CC1P
位为 0,则当 TIMx_CNT<TIMx_CCR1
时,输出高电平;如果 CCER
寄存器的 CC1P
位为 1,则当 TIMx_CNT<TIMx_CCR1
时,输出低电平(手册 P379)
CCR1
寄存器:捕获/比较值寄存器:设置比较值。计数器值TIMx_CNT
与通道 1 捕获比较寄存器CCR1
进行比较,通过比较结果输出有效电平和无效电平OC1REF=0
无效电平OC1REF=1
无效电平
TIMx_CCMR1
寄存器:OC1M[2:0]
位:用于设置 PWM 模式- 110:PWM 模式 1
- 111:PWM 模式 2
CCER
寄存器:CC1P
位:输入/捕获 1 输出极性。- 0:高电平为有效电平
- 1:低电平为有效电平
CCER
寄存器:CC1E
位:输入/捕获1输出使能。- 0:关闭使能
- 1:打开使能
- 输出电平信号
参数状态
占空比
duty circle = TIM3->CRR1 / ARR
其中 TIM->CRR1
为用户设定值, ARR
为重装载值
频率
Fpwm = Tclk / ((ARR + 1)*(PSC + 1))
其中 ARR
为重装载值, PSC
为预分频值
配置
由于板子上蜂鸣器的引脚是 PD14,而且这个引脚是接在 TIM4_CH3
上的,所以可以直接点击该引脚,并且设置为 TIM4_CH3
。然后再设置 TIM4
定时器,设置时钟源和通道 3 为 PWM 工作模式,并且设置其工作模式为 PWM1 号工作模式。设置预分频为 1-1
,设置 ARR
为 21000-1
表示 PWM 的频率为 4000 Hz(84000000/1/21000)。
然后生成代码,注意要在初始化时,将 TIM4
计数开始,并且启动对应定时器中对应的通道的 PWM
HAL 库中 PWM 的控制函数
1 | //PWM启动函数 |
设置占空比可以直接修改对应通道的 CRRx
寄存器即可
时钟输入捕获
简介
定时器输入捕获脉宽,测量时间。所谓捕获就是通过检测捕获通道上的边沿信号。在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存器(TIMx_CCR)里面,完成一次捕获
原理
通用定时器在输入捕获(Input Capture, IC)模式下,当通道输入引脚出现指定电平跳变时,当前 CNT 的值将被锁存到 CCR 中,常被用于测量 PWM 波形的频率,占空比,脉冲间隔,电平持续时间等参数。每个高级定时器和通用定时器都拥有 4 个输入捕获通道,并且可配置为 PWMI 模式,用来同时测量频率和占空比。可配合主从触发模式,实现硬件全自动测量
可以用于测量脉冲宽度或者测量频率。下图是输入捕获测量高电平脉宽的原理
图中的 t1~t2 时刻就是需要测量的高电平的时间。测量方法:设置定时器通道 x 为上升沿捕获,这样在 t1 时刻就会捕获到当前的 CNT 值,然后立即清零 CNT,并且设置通道 x 为下降沿捕获,这样到 t2 时就又会发生捕获事件,得到此时的 CNT 的值。然后根据定时器的计数频率,就可以算出 t1~t2 之间的时间,从而得到高电平脉冲的时间
在 t1~t2 之间,可能产生 N 次定时器溢出,这就要求我们对定时器的溢出做处理,防止高电平太长,导致数据不准确。对于上图中,由于 t1~t2 之间可能产生 N 次中断,所以我们需要对每次溢出中断做处理,记录其中溢出的次数 N,从而得到持续总时间 T = N * ARR + CCRx2 - CCRx1
就是通过检测 TIMx_CHx 上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)里面,完成一次捕获。同时还可以配置捕获时是否触发中断/DMA 等
寄存器
TIMx_ARR
自动重装载值TIMx_PSC
时钟预分频系数OCxREF
参考信号TIMx_CCMR1
捕获/比较模式寄存器,各段描述如下。通道方向通过配置相应的CCxS
位进行定义。此寄存器的所有其他位再输入模式和输出模式下的功能均不同。对于任一给定位OCxx
用于说明通道配置为输出时该位所对应的功能,ICxx
则用于说明通道配置为输入时该位对应的功能。所以需要注意同一个位在输入阶段和输出阶段都具有不同的涵义OC2CE
输出比较 2 清零使能- 0:
OC2Ref
不受ETRF
输入影响 - 1:
ETRF
输入上检测到高电平时,OC2Ref
立即清零
- 0:
OC2M[2:0]
输出比较 2 模式- 这些位提供 OC2 和 OC2N 的输出参考信号
OC2REF
的行为。OC2REF
为高电平有效,而 OC2 和 OC2N 的有效电平则取决于 CC2P 和 CC2NP 位 - 000:冻结——输出比较寄存器
TIMx_CCR1
与计数器TIMx_CNT
进行比较不会对输出造成任何影响 - 001:将通道 2 设置为匹配时输出有效电平。当计数器
TIMx_CNT
与捕获/比较寄存器 2TIMx_CCR2
匹配时,OC1REF
信号强制变为高电平 - 010:将通道 2 设置为匹配时输出无效电平。当计数器
TIMx_CNT
与捕获/比较寄存器 2TIMx_CCR2
匹配时,OC2REF
信号强制变为低电平 - 011:翻转,当
TIMx_CNT=TIMx_CCR2
时,OC2REF
发生翻转 - 100:强制变为无效电平,
OC1REF
强制变为低电平 - 101:强制变为有效电平,
OC1REF
强制变为高电平 - 110:PWM 模式 1,在递增计数模式下,只要
TIMx_CNT<TIMx_CCR2
,通道 2 便为有效状态,否则为无效状态。在递减计数模式下,只要TIMx_CNT>TIMx_CCR2
,通道 2 便为无效状态(OC1REF=0
),否则为有效状态(OC1REF=1
) - 111:PWM 模式 2,在递增计数模式下,只要
TIMx_CNT<TIMx_CCR2
,通道 2 便为无效状态,否则为有效状态。在递减计数模式下,只要TIMx_CNT>TIMx_CCR2
,通道 2 便为有效状态,否则为无效状态。 - 在 PWM 模式 1 或 PWM 模式 2 下,仅当比较结果发生改变或输出比较模式由冻结模式切换到 PWM 模式时,
OCREF
电平才会发生更改。
- 这些位提供 OC2 和 OC2N 的输出参考信号
OC2PE
输出比较 2 预装载使能- 0:禁止与
TIMx_CCR2
相关的预装载寄存器。可随时向TIMx_CCR2
写入数据,写入后将立即使用新值。 - 1:使能与
TIMx_CCR2
相关的预装载寄存器。可读/写访问预装载寄存器。TIMx_CCR1
预装载值在每次生成更新事件时都会装载到活动寄存器中
- 0:禁止与
OC2FE
输出比较 2 快速使能:434CC2S[1:0]
捕获/比较 2 选择- 00:CC2 通道配置为输出
- 01:CC2 通道配置位输入,IC2 映射到 TI2 上
- 10:CC2 通道配置为输入,IC2 映射到 TI1 上
- 11:CC2 通道配置为输入,IC2 映射到 TRC 上。此模式仅在通过 TS 位(
TIMx_SMCR
寄存器)选择内部触发输入时有效 - 只有当通道关闭时(
TIMx_CCER
中的CC2E=0
),才可向CC2S
位写入数据
IC2F[3:0]
输入捕获 2 滤波器,此位域可定义TI2
输入的采样频率和适用于TI2
的数字滤波器带宽。数字滤波器由事件计数器组成,每 N 个事件才视为一个有效边沿- 0000:无滤波器,按 fDTS 频率进行采样 1000:fSAMPLING=fDTS/8,N=6
- 0001:fSAMPLING=fCK_INT,N=2
- 1001:fSAMPLING=fDTS/8,N=8
- 0010:fSAMPLING=fCK_INT,N=4
- 0011:fSAMPLING=fCK_INT,N=8
- 0100:fSAMPLING=fDTS/2,N=6
- 0101:fSAMPLING=fDTS/2,N=8
- 0110:fSAMPLING=fDTS/4,N=6
- 0111:fSAMPLING=fDTS/4,N=8
- 1010:fSAMPLING=fDTS/16,N=5
- 1011:fSAMPLING=fDTS/16,N=6
- 1100:fSAMPLING=fDTS/16,N=8
- 1101:fSAMPLING=fDTS/32,N=5
- 1110:fSAMPLING=fDTS/32,N=6
- 1111:fSAMPLING=fDTS/32,N=8
IC2PSC[1:0]
输入捕获 2 预分频器,此位域定义CC2
输入(IC1
)的预分频比,只要CC2E=0
(TIMx_CCER
寄存器),预分频器便立即复位- 00:无预分频器,捕获输入上每检测到一个边沿便执行捕获
- 01:每发生 2 个事件便执行一次捕获
- 10:每发生 4 个事件便执行一次捕获
- 11:每发生 8 个事件便执行一次捕获
TIMx_CCER
捕获/比较使能寄存器,此处使用到这个寄存器的最低 2 位CC1P
捕获比较 1 的输出极性- CC1 通道被设置为输出
- 0:OC1 高电平有效
- 1:OC1 低电平有效
- CC1 通道被设置为输入:
CC1NP, CC1P
位可针对触发或捕获操作选择TI1FP1
和TI2FP1
的极性- 00:非反相/上升沿触发。电路对
TIxFP1
上升沿敏感(在复位模式、外部时钟模式或触发模式下执行捕获或触发操作),TIxFP1
未反相(在门控模式或编码器模式下执行触发操作) - 01:反相/下降沿触发。电路对
TIxFP1
下降沿敏感(在复位模式、外部时钟模式或触发模式下执行捕获或触发操作),TIxFP1
反相(在门控模式或编码器模式下执行触发操作) - 10:保留,不使用此配置
- 11:非反相/上升沿和下降沿均触发。电路对
TIxFP1
上升沿和下降沿都敏感(在复位模式、外部时钟模式或触发模式下执行捕获或触发操作),TIxFP1
未反相(在门控模式下执行触发操作)。编码器模式下不得使用此配置。
- 00:非反相/上升沿触发。电路对
- CC1 通道被设置为输出
CC1E
捕获/比较 1 输出使能- CC1 通道配置为输出
- 0:关闭,OC1 未激活
- 1:开启,在相应输出引脚上输出 OC1 信号
- CC1 通道配置为输入,此位决定了是否可以实际将计数器值捕获到输入/捕获寄存器中
- 0:禁止捕获
- 1: 使能捕获
- CC1 通道配置为输出
CC1NP
捕获/比较 1 输出的极性- CC1 通道被配置为输出,CC1NP 必须保持清零
- CC1 通道被配置为输入,与 CC1P 配合使用,用以定义
TI1FP1, TI2FP1
的极性
- 保留位:必须保持复位值
TIMx_DIER
DMA/中断使能寄存器- 保留位:必须保持复位值
TDE
触发 DMA 请求使能- 0:禁止触发 DMA 请求
- 1:使能触发 DMA 请求
CCxDE
捕获/比较 x DMA 请求使能- 0:禁止 CCx DMA 请求
- 1:使能 CCx DMA 请求
UDE
更新 DMA 请求使能- 0:禁止更新 DMA 请求
- 1:使能更新 DMA 请求
TIE
触发信号TRGI
中断使能- 0:禁止触发信号中断
- 1:使能触发信号中断
CCxIE
捕获/比较 x 中断使能- 0:禁止 CCx 中断
- 1:使能 CCx 中断
UIE
更新中断使能- 0:禁止更新中断
- 1:使能更新中断
TIMx_CCRx
捕获比较寄存器CCRx[31:16]
捕获比较 x 的高 16 位CCRx[15:0]
捕获比较 x 的低 16 位- 如果通道 CCx 配置为输出:
CCRx
是捕获/比较寄存器 x 的预装载值。如果没有通过TIMx_CCMR
寄存器中的OC1PE
位来使能预装载功能,写入的数值会被直接传输到当前寄存器中。否则只在发生更新事件时生效(拷贝到实际起作用的捕获/比较寄存器 x) - 如果通道 CCx 配置为输入:
CCRx
是上一个输入捕获 x 事件发生时的计数器值
- 如果通道 CCx 配置为输出:
配置
这里配置引脚为 PB0,选择 TIM3_CH1,然后将其设置为 Input Capture direct mode 直接输入捕获模式
设置参数如上图,为了保证获得输入脉冲准确,所以设置的比较精细
然后直接生成代码,生成代码之后还需要做一些初始化的配置捕获中断和定时器中断回调函数,函数介绍以及例子如下
1 | // 设置时钟的 CNT |
定时器 PWM 输入模式
简介
为了方便用户使用,STM32 设置了专门的 PWM 输入捕获模式,在这种模式下,输入捕获更加快捷,相比于普通的输入捕获模式来说,该模式代码量很小,能够快速实现对 PWM 周期,占空比,频率的测量。但是在 STM32F405 芯片上只有 TIM8 和 TIM1 可以被配置为 PWM 输入模式
原理
该模式是输入捕获模式的一个特例,与输入捕获模式的区别如下
- 两个
ICx
信号被映射到同一个TIx
输入 - 这两个
ICx
信号为边沿有效,但是极性相反 - 其中一个
TIxFP
信号被作为触发输入信号,而从模式控制器被配置为复位模式 - 选择
TIMx_CCR1
的有效输入:设置TIMx_CCMR1
寄存器的CC1S=01
(选中TI1
) - 选择
TI1FP1
的有效极性(用来捕获数据到TIMx_CCR1
中和清除计数器),置CC1P=0
(上升沿有效) - 选择
TIMx_CCR2
的有效输入:设置TIMx_CCMR1
寄存器的CC2S=10
(选中TI1
) - 选择
TI1FP1
的有效极性(用来捕获数据到TIMx_CCR2
),置CC1P=1
(下降沿有效) - 选择有效的触发输入信号:设置
TIMx_SMCR
寄存器中的TS=101
(选择TI1FP1
) - 配置从模式控制器为复位模式:设置
TIMx_SMCR
寄存器中的SMS=100
- 使能捕获:设置
TIMx_CCER
寄存器中的CC1E=1
且CC2E=1
配置
这里配置将输入模式通道 1 设置为直接捕获输入,并且为上升沿捕获
将输入模式通道 2 配置为非直接捕获输入,配置为下降沿捕获
打开时钟捕获比较中断
代码使用
1 | HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel); // 开启PWM |
代码实现
1 | // 启动 PWM 和 输入捕获 |
单脉冲模式
介绍
单脉冲模式是 PWM 模式的一种特例,在这种模式下,计数器可以在一个激励信号的触发下启动,并且可以在一段可编程的延迟之后产生一个脉宽可编程的脉冲
可以通过从模式控制器启动计数器。可以在输出比较模式或 PWM 模式下生成波形。将 TIMx_CR1
寄存器中的 OPM
位置 1,即可选择单脉冲模式。这样,发生下一更新事件 UEV 时,计数器将自动停止
当然需要注意的是,只有当比较直与计数器的初始值不同时,才能正确产生一个脉冲。启动前,也就是定时器等待触发时,必须完成配置为 CNT < CCRx < ARR
,当然也要注意 CCRx > 0
如下图所示,单脉冲模式实际上就是产生一个周期的 PWM 输出之后,定时器更新的时候停止计数,然后就不会有下一周期的脉冲产生了,以此来实现单脉冲模式输出
配置
将 TIM1 的 CH2 配置为 PWM Generation CH2 模式,也就是 PWM 输出模式,使能 One Pulse Mode
配置参数,设置 CH Polarity 为 Low。这里设置预分频系数和 ARR
如图中所示,然后设置 PWM 输出参数的 Pulse 为 50 也就是 CRRx
。剩下的分频系数,计数周期,输出比较值等都可根据实际需求进行设置,用于控制延时事件和脉冲宽度
串口配置
简介
UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作 UART。它将要传输的资料在串行通信与并行通信之间加以转换。作为把并行输入信号转成串行输出信号的芯片, UART 通常被集成于其他通讯接口的连结上。
USART:通用同步/异步串行接收/发送器,(Universal Synchronous/Asynchronous Receiver/Transmitter) USART 是一个全双工通用同步/异步串行收发模块,该接口是一个高度灵活的串行通信设备。
串口接口通过三个引脚与其它设备相连。任何 USART 双向通信至少需要两个引脚:接受输入(RX)和数据输出(TX)
- RX:接收数据串行输入。通过过采样技术来区别数据和噪音,从而恢复数据。
- TX:发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,并且不发送数据时, TX引脚处于高电平。在单线和智能卡模式里,此I/O口被同时用于数据的发送和接收。
串口发送接收有三种基本方式
- 轮询(阻塞模式):
- 阻塞发送:使用
HAL_UART_Transmit
函数,使用超时机制管理 - 阻塞接受:使用
HAL_UART_Receive
函数,使用超时机制管理
- 阻塞发送:使用
- 中断模式:
- 中断发送:使用
HAL_UART_Transmit_IT
函数 - 中断接收:使用
HAL_UART_Receive_IT
函数
- 中断发送:使用
- DMA
- DMA 发送:使用
HAL_UART_Transmit_DMA
函数 - DMA 接收:使用
HAL_UART_Transmit_DMA
函数
- DMA 发送:使用
流控
流控概念来源于 RS232 这个标准,在 RS232 中包含了串口与流控的定义。其中 RS 就是 Recommend Standard 的缩写。没有被委员会制定,所以不同厂商做 RS232 时标准不太一样
存在意义
当两台设备进行串口通讯时,它们对数据处理速度不同。如果接收端数据缓冲区满了就会丢失后续发送过来的数据。使用流控是为了当接收端数据处理能力饱和时就发出信号使得发送端停止发送,直到接收端处理量减少,发送继续发送的信号给发送端,发送端才会继续发送数据
硬件流控
硬件流控一般通过 CTS(Clear to Send)和 RTS(Request to Send)两个引脚实现,如下
对于 USART1 给 USART2 发送数据时,USART1 会使得 RTS 引脚有效,表明其想要发送请求发送的信息给 USART2,而 USART1 会检测对应的来自于 USART2 的 CTS 引脚,直到 CTS 引脚有效才会真正开始发送数据。并且接下来在发送每个字节之前都会检测对应的 CTS 是否有效,如果有效才会继续传输数据,否则就不能再发送了。如果在这期间 USART2 的 CTS 引脚一直有效,那么当 USART1 发送结束之后,就会把 RTS 设置为无效,表示数据发送完毕了
软件流控
软件流控就是以一个特殊的字符或者报文来表示接收端已经不能接收新的数据了,基本的流程就是在接收端接收数据很多的时候或者主动发送给发送端一个特殊字符或者报文,当发送端接收到这个特殊字符之后就不能再发送数据了
软件流控很容易实现,所以渐渐地代替掉了硬件流控
STM32 的硬件流控
部分型号的 STM32 的 USART 具有硬件流控的功能,可以实现 RS232 的硬件流控。还有部分型号的 STM32 具有 RS485 的硬件流控功能。
阻塞模式
发送函数
1 | HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); |
该函数的作用就是将数据通过 UART
发送到外部的设备或者其他设备,其中的参数含义如下
huart
串口的句柄,表示要使用的串口外设pData
要发送的数据的首地址Size
要发送的数据的大小,以字节为单位Timeout
发送时的超时时长,单位为毫秒。如果在指定的时间内发送成功则返回HAL_OK
,否则返回HAL_TIMEOUT
。如果设置为HAL_MAX_DELAY
,则处理器会一直等待直到数据发送完成再执行下一条语句
接收函数
1 | HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); |
作用是从串口接收数据,并将接收到的数据存储在指定的缓冲区中
huart
要使用的串口句柄地址pData
要接收的数据缓冲区首地址Size
要接收的数据长度,以字节为单位Timeout
超时时间,如上
配置
这里选择串口 1,模式选择 Asymchronous
异步,下面的参数不需改变,直接生成代码即可
波特率设置为 115200,字长设置为 8 位,停止位设置为 1 位,其他都按照默认参数即可。而且下面的数据方向一定要是 Receive and Transmit
表示发送和接收
代码实现和测试
发送:
1 | uint8_t data[] = "hello"; |
接收:
1 | while (1) |
可以使用 USB 转串口与 vofa 来测试
中断模式
发送函数
1 | HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); |
这个函数作用是启动 UART
传输并以非阻塞的方式发送一定数量的数据,参数如下
huart
要使用的串口句柄地址pData
发送缓冲区的首地址,存放要发送的数据Size
要发送的缓冲区的长度,也就是要发送的字节数
接收函数
1 | HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); |
这个函数用于通过中断接收串口发送的消息,其中参数如下
huart
要使用的串口句柄地址pData
接收缓冲区的地址,存放接收到的数据Size
接收缓冲区的长度,也就是要接收到的字节数
中断模式的函数的前三个参数和阻塞的方式完全一致,只是没有超时时间管理,因为中断方式配置完成寄存器之后不会占用 CPU 了,会在接收或者发送完成之后触发中断,所以不需要时间管理
配置
只需要在上述串口配置完成的基础上,打开 NVIC Setting,勾选 Enable 即可
代码实现和测试
首先在初始化之后,循环之前添加一个开启一次接收中断,表示接收完成 sizeof(data)
个字节之后会进入接收完成中断,也就是 HAL_UART_RxCpltCallback
函数中,然后在这个函数中执行中断发送任务,也就是接收完成之后执行发送
1 | HAL_UART_Receive_IT(&huart1, data, sizeof(data)); |
DMA 模式
介绍
DMA (Direct Memory Access 直接访问内存)是一种计算机系统中用于数据传输的机制。允许数据在外设和内存之间传输而不通过 CPU,从而减轻了 CPU 的负担,提高数据传输的效率
发送函数
1 | HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); |
用于利用 DMA 来发送数据,参数如下
huart
要使用的串口句柄地址pData
发送缓冲区的首地址,存放要发送的数据Size
要发送的缓冲区的长度,也就是要发送的字节数
接收函数
1 | HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); |
这个函数用于利用 DMA 来接收数据,将数据直接写入到 pData
中,其中参数如下
huart
要使用的串口句柄地址pData
接收缓冲区的地址,存放接收到的数据Size
接收缓冲区的长度,也就是要接收到的字节数
配置
首先是配置 DMA,如下,需要点击 Add
来添加 DMA 接收或者发送。这里将 DMA 的模式设置为 Circular
表示循环接收数据,可以多次接收数据,而 normal
只能接收一次数据
首先是必须要开启中断,也就是上述的中断发送中的中断使能,如下
代码实现和测试
由于 DMA 接收结束之后也是会直接触发串口接收完成中断的,所以这里与上述的串口中断相似
1 | HAL_UART_Receive_DMA(&huart1, data, sizeof(data)); |
总结
在中断和 DMA 模式下,都需要在初始化完成之后开启一次中断或者 DMA 接收,并且在接收回调函数中重启一次中断或者 DMA 接收才能实现循环接收。
串口配置时模式选择
Mode | 描述 | 硬件引脚 | 外设支持 |
---|---|---|---|
Asynchronous | 异步模式 | TXD,RXD | USART,UART |
Synchronous | 同步模式 | TXD,RXD,CK | USART |
Single Wire (Half-Duplex) | 半双工单线模式 | TXD | USART,UART |
Multiprocessor Communication | 多处理器通讯模式 | TXD,RXD | USART,UART |
IrDA | 红外解码通信 | TXD,RXD | USART,UART |
LIN | 总线通信 | TXD,RXD | USART,UART |
SmartCard | 智能卡模式 | TXD | USART,UART |
SmartCard with Card Clock | 带时钟智能卡模式 | TXD,CK | USART |
CAN 配置
介绍
CAN 就是 Controller Area Network 控制器局域网,是 ISO 国际标准化的串行通信协议。
特点
- 多主控制:总线空闲时,所有单元都可发送消息,而且两个以上的单元同时开始发送消息时,根据标识符决定优先级,并且对各消息 ID 的每个位进行逐个比较。比较之后优先级高的可继续发送消息,反之则立即停止发送而进行接收工作
- 系统柔软性:连接总线的单元,没有类似的地址的信息,只有发送消息的 ID 的分别,所以在总线上添加单元时,已连接的其他单元的软硬件和应用层都不需改变
- 速度快,距离远:短距离(<40m)时最高 1Mbps,最远可以达到 10km(速率<5kbps)
- 具有错误检测,错误通知和错误恢复功能:所有单元都可以检测错误,检测出错的单元会立即通知其他单元,正在发送消息的单元一旦被检测到错误会强制结束当前的发送。强制结束发送的单元会不断反复地重新发送此消息直到发送成功为止。
- 故障封闭功能:CAN 可以判断出错误的类型时总线上暂时的数据错误(外部噪声)还是持续的数据错误(单元内故障,驱动器故障,断线)。所以当总线上发生持续的数据错误时,可以将引起此故障的单元从总线上隔离
- 连接节点多:CAN 总线是可以同时连接多个单元的总线。可连接的单元总线数量理论上是没有限制的,但是实际上可连接的单元数收到总线上的时间延迟以及电气负载的限制。降低通信速度,可连接的单元数增加提高通讯速度,则可连接的单元数减少
数据帧
数据帧是 CAN 通信中 5 中类型帧中最常用且最复杂的,数据帧由 7 个段组成
- 帧起始:表示数据帧开始的段。标准帧和扩展帧都是由1个位的显性电平表示帧起始。
- 仲裁段:表示该帧优先级的段。标准帧的格式与扩展帧格式在这里是有区别的
- 标准帧:包括
ID
和RTR
,标准帧一般为 11 位 - 扩展帧:包含
ID
,RTR
,SRR
和IDE
,扩展帧一般为 32 位,实际上是 29 位 ID
:高位在前,低位在后,设置的 IDRTR
:远程请求位- 0:数据帧
- 1:远程帧
SRR
:替代远程请求位,设置为 1IDE
:标识符选择位- 0:标准标识符
- 1:扩展标识符
- 标准帧:包括
- 控制段:表示数据的字节数以及保留位的段。由 6 个位构成,表示数据段的字节数,标准帧和扩展帧的控制段会有点不同,如图中所示
r0, r1
:保留位。必须以显性电平发送,但是接收可以是隐性电平DLC
:数据长度码,范围 0~8 字节,表示接收或者发送的数据长度IDE
:标识符选择位- 0:标准标识符
- 1:扩展标识符
- 数据段:数据的内容,一帧可发送 0~8 个字节的数据,从最高位开始发送。标准帧和扩展帧在这个段的格式完全一样
CRC
段:检查帧的传输错误的段。用于检查帧的传输错误,由 15 个位的CRC
顺序和 1 个位的CRC
界定符(用于分隔的位),标准帧和扩展帧在这个段的格式一致。CRC
的值计算范围包括:帧起始,仲裁段,控制段和数据段,接收方以相同的算法计算CRC
的值来进行比较,不一致会通报错误ACK
段:表示确认正常接收的段,此段用来确认是否正常接收,由ACK Slot
和ACK
界定符两个位组成。标准帧和扩展帧在这个段的格式也是相同的- 发送单元的
ACK
段会发送两个隐性位 - 接受单元的
ACK
段接收到正确消息的单元在ACK Slot
发送显性位,通知发送单元正常接收结束 - 上述的两种就是 发送
ACK
和 返回ACK
- 发送单元的
- 帧结束:表示数据帧结束的段,由7个位的隐性位组成。标准帧和扩展帧在这个段格式完全一样
CRC 检测
CRC 校验的基础就是多项式,生成多项式就是发送方和接收方约定的一个除数,发送方和接收方都使用这个相同的除数进行模 2 运算,结算结果相同则说明传输数据没问题,而如果计算结果不同则可能出现了问题,就是为了保证传输数据的可靠性
这里的 CAN 通信采取的是 CRC-15 的校验,多项式表示为
也就是设置的除数就是 1100010110011001
,然后根据需要传输的数据使用模 2 除法算出余数来作为 CRC
段的数据
例如,要传输的数据(也就是帧起始,仲裁段,控制段和数据段)如下
- 帧起始:1 显性电平
- 仲裁段:
- ID:0x200
- RTR:0
- 控制段:
- IDE:0
- r0:1
- DLC:1
- 数据段:只有 8 位,为
0x0A
最后可以得到一串数字,为 1 01000000000 0 0 1 1 00001010
进行模 2 除法(模 2 除法计算器)可以得到余数为 0011010011101001
,这个就是 CRC
段的值
ACK 段分析
CAN总线是一种基于广播的通讯方式,为了保证总线上的每一个正常节点都能正确的接收到报文,报文的发送者要求每一个接收节点在报文发送结束前要作出应答,这就是 ACK 段存在的意义
在一帧报文中 ACK 段长度为 2 个位,包含应答间隙 ACK Slot
和应答界定符 ACK Delimter
ACK Slot
所有接收到匹配CRC
序列的单元会在应答间隙期间用一个显性的位写入发送器的隐性位来做出回答ACK Delimter
它是ACK
的第二个位,并且是一个必须是隐性的位,所以应答间隙被两个位所包含,就是CRC
界定符位和ACK
界定位
当一条消息发送出去之后,总线上并没有 ACK 应答,那么发送器就会发送一个错误标志,并且发送错误计数器自增 8,节点就会对报文进行重新发送,不断循环。在发送错误计数器累计到 128 时,由错误主动转为错误被动状态
ACK 出错原因
- 总线上只有一个有效节点,发送报文的节点在发送出一帧报文之后会检测总线上应答间隙的状态,如果检测到应答间隙为隐性位,表示该报文没有得到
ACK
,也就是发送失败,需要重新发送,直到该节点关闭 - 波特率不匹配或节点没有初始化,导致没有
ACK
- 总线线缆短路,断路,反接
- 高速 CAN 总线上接的节点不是高速 CAN 而是容错低速 CAN,导致不匹配
总线状态
CAN 通信控制根据 CAN_L 和 CAN_H 两个引脚上的电位差来判断总线电平。总线电平分为显性和隐形电平,总线的状态必定是其中之一。发送方通过使得总线电平发生变化来将消息传递
- 隐性电平:CAN_H 与 CAN_L 电平之差为 2V 左右
- 显性电平:CAN_H 与 CAN_L 电平之差为 0V
需要注意的是,只要有一个单元输出显性电平,那这个总线上就是显性电平,只有所有的单元都输出隐性电平,总线上才会使隐性电平。
另外在 CAN 总线的起止端都有一个 120Ω 的终端电阻,用来做阻抗匹配以减少回波反射
当然 CAN_L 与 CAN_H 在隐性电平时也不是 0V,而是大约 2.5V
CAN 通信是以如下 5 种类型的帧来进行发送的
帧类型 | 用途 |
---|---|
数据帧 | 用于发送单元向接收单元传送数据的帧 |
遥控帧 | 用于接收单元向相同 ID 的发送单元请求数据的帧 |
错误帧 | 用于当检测出错误时向其他单元通知错误的帧 |
过载帧 | 用于接收单元通知其尚未做好准备接收的帧 |
间隔帧 | 用于将数据帧及遥控帧与前面的帧分离开来的帧 |
CAN 工作模式
CAN 有三种主要工作模式:初始化,正常和睡眠。硬件复位之后,CAN 进入睡眠模式以降低功耗,同时 CANTX 上的内部上拉电阻激活。软件将 MCR
寄存器的 INRQ
或 SLEEP
位置 1,以请求 CAN 进入初始化或睡眠模式。一旦进入该模式,CAN 即将 MSR
寄存器的 INAK
或 SLAK
位置 1,以确认该模式,同时禁止内部上拉电阻。如果 INAK
和 SLAK
均未置 1,则 CAN 将处于正常模式。进入正常模式之前,CAN 必须始终在 CAN 总线上实现同步。为了进行同步,CAN 将等待 CAN 总线空闲(即已监测到 CAN_RX 上的 11 个隐性位)
- 初始化模式:当硬件处于初始化模式时,可以进行软件初始化
- 进入初始化模式:设置
MCR
寄存器的INRQ
为 1,请求 CAN 进入初始化模式,等待硬件对MSR
寄存器的INAK
置位来确认进入了初始化模式。进入初始化模式不会改变配置寄存器 - 退出初始化模式:设置
MCR
寄存器的INRQ
为 0,请求 CAN 退出初始化模式,等待硬件对MSR
寄存器的INAK
复位来确认退出了初始化模式 - 初始化模式中:禁止报文发送和接收,CAN_TX 引脚输出隐性位。软件对 CAN 的初始化必须至少包括位时间特性(CAN_BTR)和控制(CAN_MCR)这2个寄存器。
- 进入初始化模式:设置
- 正常模式:一旦初始化完成,软件必须向硬件请求进入正常模式,这样才能在 CAN 总线上进行同步,并开始接收和发送
- 进入正常模式:在初始化完成之后,软件应该让硬件进入正常模式,以便正常接收和发送报文。软件可以通过对
MCR
寄存器的INRQ
位清零请求进入正常模式,然后要等待硬件对MSR
寄存器的INAK
复位来确认进入了正常模式。 - CAN 进入正常模式之后,并与 CAN 总线上的数据传输实现同步后,即可参与总线活动。执行这一步需要等待出现一个由 11 个连续隐性位(总线空闲状态)组成的序列
- 进入正常模式:在初始化完成之后,软件应该让硬件进入正常模式,以便正常接收和发送报文。软件可以通过对
- 睡眠模式:为降低能耗功耗,CAN 具有低功耗模式,该模式下,CAN 时钟停止,但软件仍可访问 CAN 邮箱
- 进入睡眠模式:软件将
MCR
寄存器的SLEEP
位置位而发出请求之后即可进入该模式 - 睡眠模式中:CAN 始终将停止,但是软件仍可访问 CAN 邮箱
- 退出睡眠模式:软件通过设置
MCR
的INRQ
位为 1 来请求进入初始化模式,则必须同时将SLEEP
清零。软件将SLEEP
清零之后或者检测到 CAN 总线活动时,CAN 即被唤醒。检测到 CAN 总线活动时,如果MCR
寄存器的AWUM
置位,硬件将通过清零SLEEP
位来自动执行唤醒序列。如果AWUM
位清零,在发生唤醒中断时,软件必须将SLEEP
位清零才能退出睡眠模式 - 如果使能唤醒中断,即使 CAN 自动执行唤醒序列,一旦检测到 CAN 总线活动,也会发生唤醒中断
- 将
SLEEP
清零之后,一旦 CAN 与总线同步,就会退出睡眠模式。一旦硬件将SLAK
清零,即会退出睡眠模式
- 进入睡眠模式:软件将
CAN 测试模式
可以通过 CAN_BTR
寄存器的 SILM
位和 LBKM
位来选择测试模式,这些位都必须在 CAN 处于初始化模式时进行配置。选择测试模式后,必须复位 CAN_MCR 寄存器中的 INRQ 位才能进入正常模式
- 静默模式:可以通过将
CAN_BTR
寄存器的SILM
位置位,将其置于静默模式。在该模式下可接受有效数据帧和有效遥控帧,但仅在 CAN 总线上发送隐性位,无法启动发送。如果 CAN 必须发送一个显性位(ACK 位、溢出标志、活动错误标志),该位将在内部被改道发送,以便 CAN 内核可以监视该显性位,但 CAN 总线可以保持隐性状态。静默模式可用于分析 CAN 总线上的流量,同时又不会因发送显性位(确认位、错误帧)对其造成影响 - 环回模式:可以通过将
CAN_BTR
寄存器的LBKM
位置位来设置为回环模式。该模式下,CAN 会将自身发送的消息作为接收的消息来处理并存储(如果这些消息通过了验收筛选)在接收邮箱中。该模式为自检功能提供。为了不受外部事件的影响,CAN 内核在环回模式下将忽略确认错误(在数据/远程帧的确认时隙不对显性位采样) - 静默模式与环回模式组合:可以通过将
CAN_BTR
寄存器中LBKM
和SILM
置位,将回环模式与静默模式组合起来。该模式可用于热自检。也就是 CAN 可以像在环回模式下进行检测,同时又不会影响到与 CAN_TX 和 CAN_RX 引脚相连的运行中的 CAN 系统。此模式下 CAN_RX 引脚与 CAN 断开连接,而 CAN_TX 保持隐性
错误分析
错误状态寄存器 ESR
REC[7:0]
接收错误计数器TEC[7:0]
发送错误计数器- 保留位,必须保持复位
LEC[2:0]
上一个错误代码- 000:无错误
- 001:位置填充错误:通讯线缆上传输信号违反“位填充”规则时发生该错误
- 010:格式错误:传输的数据帧与任何一种合法的帧格式不符合时发生的错误
- 011:确认错误 ACK:发送节点在
ACK
段没有接送到应答信号时发生该错误 - 100:隐性位错
- 101:显性位错
- 110:CRC 错误:发送节点计算得到的 CRC 值与接收到的 CRC 值不同时发送该结果
- 111:由软件设置
BOFF
总线关闭标志,此位由硬件在进入关闭状态时置位,TEC
上溢(超过 255)时,进入总线关闭状态,此时硬件将其置位EPVF
错误被动标志:达到错误被动极限(接收错误计数器或者发送错误计数器>127)时由硬件置位EWGF
错误警告标志:达到警告极限(接收错误计数器或发送错误计数器>95)时由硬件置位
三种错误状态
- 发送错误
TEC
(主动错误):如果接收期间发生错误,该计数器将以 1 或者 8 递增,具体取决于 CAN 标准所定义的错误状态,每次成功接收,该计数器按 1 递减,如果其数值大于 128,则复位为 120,计数器值超过 127 时,控制器进入错误被动状态。如果接收计数器的值为 0,则保持为 0。如果在于 127,则接收计数器的值应设置为 119~127 之间的值 - 接收错误
REC
(被动错误):如果发送阶段发送节后发送一个错误标志时,错误计数器以 8 累增。当成功发送一帧报文之后,发送计数器应自减 1,直到为 0 - 离线:当发送错误向上溢出时(超过255),此节点进入离线状态。处于离线状态的节点不会对总线产生任何影响,它将不会发送消息帧,ACK,错误帧,过载帧等,至于会不会接收总线上的数据,取决于此节点的实现。当一个处于离线状态下的节点接收到 128 次连续 11 位隐性位时,将变成主动错误状态,且同时设置发送错误计数器和接收错误计数器为0
离线恢复
当 TEC
大于 255 时,CAN 进入离线状态,同时 BOFF
位被置位,在离线状态下的 CAN 无法接收和发送报文
根据 MCR
寄存器中的 ABOM
位的设置,CAN 可以自动或者在软件的请求下,从离线状态恢复到错误主动状态。在这两种情况下,CAN 必须等待一个 CAN 标准所描述的恢复过程,也就是 CAN_RX 引脚上检测到 128 次连续 11 个隐性位
- 如果
ABOM=1
,CAN 进入离线状态之后,就自动开启恢复过程 - 如果
ABOM=0
,CAN 软件必须先请求 CAN 进入然后再退出初始化模式,随后恢复过程才被开启。在初始化模式下,CAN 不会监视 CAN_RX 引脚的状态,这样就不能完成恢复过程。为了完成恢复过程 CAN 必须工作在正常模式下
CAN 发送
发送邮箱标识符寄存器
STID
标准标识符EXID
扩展标志符IDE
标识符扩展,用于定义邮箱中消息的标识符类型- 0:标准标识符
- 1:扩展标识符
RTR
远程发送请求- 0:数据帧
- 1:遥控帧
TXRQ
发送邮箱请求。由软件置位,用于请求发送相应邮箱的内容。邮箱变为空之后,此位由硬件清零
发送优先级
- 标识符决定
- 当有超过 1 个发送邮箱在挂号时,根据标识符由小到大来决定优先级,如果相等则邮箱号小的先发送
- 发送请求次序决定
- 通过对
MCR
寄存器的TXFP
位置位,可以把邮箱配置为发送 FIFO。在此模式下,发送优先级由请求次序决定
- 通过对
发送位时序
位速率:由发送单元在非同步情况下发送的每秒钟的位数称为位速率。一个位一般可以分为如下四段
- 同步段(SS):多个连接在总线上的单元通过此段实现时序调整,同步进行接收和发送的工作。由隐性电平到显性电平的边沿或由显性电平到隐性电平的边沿最好出现在此段中
- 传播时间段(PTS):用于吸收网络上的物理延迟的段。所谓的网络的物理延迟指的是发送单元的输出延迟,总线上信号的传播延迟,接收单元的输入延迟。这个段的时间是以上各个延迟时间的和的两倍
- 相位缓冲段 1(PBS1):当信号边沿不包含于此段时,可在此进行补偿。由于各个单元以各自独立的时钟工作,细微的时钟误差会累计,PBS 段可吸收此误差。通过对缓冲段加减 SJW 来吸收误差
- 相位缓冲段 2(PBS2):如上
- 再同步补偿宽度(SJW):因时钟频率偏差,传送延迟等,各个单元有同步误差。SJW 为补偿此误差的最大值
这些段由 Time Quantum(时间段 Tq)的最小时间单位构成。
总线仲裁
同时有多个单元发送数据时,总线仲裁过程如下
- 总线空闲时,最先发送的单元获得发送优先权,一旦发送,其他单元无法抢占
- 如果多个单元同时发送,则连续输出显性电平多的单元具有较高优先级
- 仲裁对比时根据仲裁段来对比,仲裁段是位于帧起始之后的
发送流程
- 选择一个空置的邮箱(
TME
寄存器为 1) - 设置标识符,数据长度和发送数据
- 设置
TIR
寄存器的TXRQ
位为 1,请求发送 - 邮箱挂号,等待成为最高优先级
- 预定发送,对于取消发送可以将对应 CAN 的
TIR
寄存器中对应邮箱的ABRQ
标志位置位,即可取消发送 - 发送
- 邮箱空置(成功或失败都会邮箱空置,除非设置发送失败重复发送)
CAN 接收
每个 CAN 中有两个 FIFO
,每个 FIFO
中包含三个邮箱,这样设计允许CAN单元最多可以缓存六个接收到的报文。这种设计使得CAN单元能够有效地处理和存储接收到的数据,确保数据的完整性和可靠性
FIFO 寄存器 CAN_RF0R/CAN_RF1R
RFOM
释放FIFO
释放邮箱,由软件置位用于释放 FIFO 的邮箱。当FIFO
中至少有一条消息挂起时才能释放邮箱。当FIFO
为空时,该位置位无用。如果FIFO
中至少有两条消息挂起时,软件必须释放邮箱,才能访问下一条消息。每次邮箱释放之后,此位由硬件清零FOVR
当FIFO
填满之后,如果接收到新的消息并且通过了过滤器,此位将由硬件置位,由软件清空FULL
当FIFO
存储了三条消息之后,由硬件置位,由软件清空FMP
用于指示接收FIFO
中挂起的消息数。硬件每向FIFO
中存储一条新消息时,FMP
就会增加。软件每次将RFOM
置位来释放出邮箱,FMP
就会减少- 保留位:必须保持复位
需要注意的是
CAN_RF0R
用于FIFO0
的控制CAN_RF1R
用于FIFO1
的控制
接收流程
- 收到有效报文,存入到
FIFO
的一个邮箱,这个由硬件控制,不需要软件控制 - 每个
FIFO
中最多有三个邮箱,FIFO
接收到的报文数可以从CAN_RFR
寄存器的FMP[1:0]
控制位读取,只要FMP
不为 0,就可以从 FIFO 中读取收到的报文 - 软件读取邮箱内容时,通过将
CAN_RFR
寄存器的RFOM
位设置为 1,从而将邮箱释放 - 当
FIFO
中的三个邮箱都是满的,下一个有效的报文就会导致溢出,并且这个报文会丢失 - 启用
FIFO
锁定功能需要将CAN_MCR
寄存器的RFLM
位置位,那么邮箱满了之后新收到的报文就被丢弃,从FIFO
中读取到的是最早收到的三个报文。否则FIFO
中最后收到的报文会被新报文所覆盖,而新报文不会丢失
接收中断
当 FIFO
存入,存满,溢出报文时,分别会产生中断
接收消息储存位置
数据存在于 RDLR
和 RDHR
寄存器,标志 ID 和 RTR
储存在 RIR
中,数据数 DLC
和接收时通过的过滤器号 FMI
储存在 RDTR
中
CAN 过滤器
筛选器组 i 寄存器 CAN_FiRx
STM32 芯片中共有 28 个筛选器寄存器
FB[31:0]
筛选器位,标识符,寄存器每一位用于指定预期标识符相应位的级别- 0:需要显性位
- 1:需要隐性位
- 掩码:寄存器的每一位用于指定相关标识符寄存器的位是否必须与预期标识符的相应位匹配
- 0:无关,不使用此位进行比较
- 1:必须匹配,传入标识符的此位必须与筛选器相应标识符寄存器中指定的级别相同
每个筛选器组的 CAN_FiRx 都由2个32位寄存器构成,即 CAN_FiR1 和 CAN_FiR2。根据过滤器位宽和模式的不同设置,这两个寄存器的功能也不尽相同
筛选器激活寄存器 CAN_FA1R
- 保留位:必须保持复位
FACTx
筛选器激活。软件将此位置位可激活筛选器,要修改该筛选器寄存器必须将FACTx
清零或者将FMR
寄存器的FINIT
置位- 0:未激活
- 1:激活
筛选器分配寄存器 CAN_FFA1R
只有当把 FMR
寄存器设置了筛选器初始化模式 FINIT=1
时,才能对此寄存器执行写操作
- 保留位:必须保持复位
FFAx
筛选器 x 的筛选器FIFO
分配,通过此筛选器的消息将存储在指定的FIFO
中- 0:筛选器分配到
FIFO0
- 1:筛选器分配到
FIFO1
- 0:筛选器分配到
过滤器配置
在对 CAN 过滤器组(模式,位宽,FIFO关联,激活和过滤器组)初始化之前,软件要对 FMR
寄存器的 FINIT
位置位,当 FINIT
被置位之后,报文的接收被禁止。对过滤器的初始化可以在非初始化模式之下进行。可以先对过滤器
不需要在初始化模式下进行过滤器初值的设置,但必须在它处在非激活状态下完成(相应的 FACT
位为 0)。而过滤器的位宽和模式的设置,则必须在初始化模式中进入正常模式前完成
配置
这里打开 CAN1,直接使能 CAN1,然后配置参数。由于使用的是 STM32F407 的芯片,时钟树配置中,CAN1 存在于 APB1 总线上,并且时钟频率为 42MHz
配置时,预分频系数选择 7,相位缓冲段 1 设置为 4,相位缓冲段 2 设置为 1,所以就可以得到 CAN 通信的频率为 42 / 7 / (4 + 1 + 1) = 1 MHz
然后下面的几个基础的参数分别为
- DBF:冻结调试,一般是当 STM32 芯片处于程序调试模式时才使用的,平时使用并不影响
ENABLE
设置 CAN 处于禁止收发的状态,仍然可以访问接收 FIFO 中的数据DISABLE
设置 CAN 处于工作状态
- TTCM:时间触发模式
ENABLE
使能时间触发模式,它用于配置 CAN 的时间触发通信模式,在此模式下,CAN 使用它内部定时器产生时间戳,并把它保存在 CAN_RDTxR、CAN_TDTxR 寄存器中。内部定时器在每个 CAN 位时间累加,在接收和发送的帧起始位被采样,并生成时间戳。利用它可以实现 ISO 11898-4 CAN 标准的分时同步通信功能DISABLE
禁用时间触发模式
- ABOM:自动离线管理
ENABLE
使能自动离线管理。当节点检测到它发送错误或接收错误超过一定值时,会自动进入离线状态,如果启用这个模式,那么会在进入离线状态之后,适当(等待总线空闲)的自动恢复DISABLE
禁用自动离线管理
- AWUM:自动唤醒
ENABLE
启用自动唤醒功能,CAN 外设可以使用软件进入低功耗的睡眠模式,如果使能了这个自动唤醒功能,当 CAN 检测到总线活动的时候,会自动唤醒DISABLE
禁用自动唤醒功能
- NART:自动重传
ENABLE
启用报文自动重传功能,设置这个功能后,当报文发送失败时会自动重传至成功为止。若不使用这个功能,无论发送结果如何,消息只发送一次DISABLE
禁用报文自动重传功能
- RFLM:锁定模式
ENABLE
启用 FIFO 锁定模式,该功能用于锁定接收 FIFO。锁定后,当接收 FIFO 溢出时,会丢弃下一个接收的报文DISABLE
不启用 FIFO 锁定模式,不锁定接收 FIFO,则下一个接收到的报文会覆盖原报文
- TXFP:报文发送优先级判定方法
ENABLE
报文发送优先级的判定方法,以报文存入发送邮箱的先后顺序来发送DISABLE
按照报文 ID 的优先级来发送,ID 越小优先级越高
这里是开启环回模式进行测试,正常通信选 normal。然后生成代码
相关函数
1 | HAL_CAN_Start //开启CAN通讯 |
中断
CANx_SCE_IRQHandler
CAN 总线状态改变中断,通过打开这个中断,配合代码可以精确的监测 CAN 总线的故障情况,实际上就是检测 CAN 的ESR
寄存器。只要CAN总线发送状态改变,就会触发中断。可以非常灵敏的检测CAN总线断线,短路等等故障CAN1_TX_IRQHandler
CAN 发送完成中断CAN1_RX0_IRQHandler
CAN 接收中断,用于 FIFO0CAN1_RX1_IRQHandler
CAN 接收中断,用于 FIFO1
代码配置
CubeMX 生成的代码之后,需要在 CAN 初始化函数中开启 CAN 通信和使能 CAN 中断,如下
1 | HAL_CAN_Start(&hcan1); |
同时也需要配置 CAN 的过滤器
1 | CAN_FilterTypeDef sFilterConfig; |
其中
FilterActivation
CAN 过滤器使能状态FilterBank
过滤器号,两个 CAN 共享 28 个过滤器,也就是 0~27 号过滤器,但是实际上两个 CAN 分别配置的过滤器不共享。也就是 CAN1 使用了这个过滤器的位置之后,CAN2 就不能使用了FilterMode
过滤器工作模式CAN_FILTERMODE_IDMASK
掩码模式,寄存器的每一位用于指定相关标识符寄存器的位是否必须与预期标识符的相应位匹配CAN_FILTERMODE_IDLIST
列表模式,接收到的报文 ID 必须与设置的报文 ID 一致
FilterScale
过滤器中 ID 的长度CAN_FILTERSCALE_32BIT
ID 长度为 32 位,设置之后下面的 ID 会由FilterIdHigh
和FilterIdLow
连接而成,相应的掩码也会由FilterMaskIdHigh
和FilterMaskIdLow
组成。这里就意味着使用扩展帧,扩展帧长度为 29 位,所以使用时,需要左移 3 位CAN_FILTERSCALE_16BIT
ID 长度为 16 位,设置之后就以同时设置 2 个掩码匹配规则,高低位分别对应作为掩码。这意味着使用标准帧,标准帧长度为 11 位,所以需要左移 5 位
FilterFIFOAssignment
设置当前过滤器作用于哪一个FIFO
FilterIdHigh
和FilterIdLow
IDFilterMaskIdHigh
和FilterMaskIdLow
掩码 ID
另外,一定需要重新接收到消息的回调函数
1 | void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { |
还有 CAN 的发送函数,示例如下
1 | void CAN_senddata(CAN_HandleTypeDef *hcan) |
由于是环回模式,所以自己会接收到自己的消息,也就是会进入到接收中断中
FreeRtos
配置
在 CubeMX 中找到 FREERTOS,使能并且选择版本为 CMSIS_V2,然后进行参数配置,这里可以看到有很多可设置的参数,如下
- Tasks and Queues:任务与队列,用于配置任务体以及消息队列
- Timers and Semaphores:软件定时器与信号量,用于配置内核对象
- Mutexes:互斥量,用于配置内核对象
- Events:时间,用于配置内核对象
- FreeRTOS Heap Usage:查看用户任务和系统任务的堆占用
- Config Parameters:系统的参数配置
- Include Parameters:系统的功能裁剪
- Advanced Settings:CubeMX 生成代码预配置项
- User Constants:用户常量定义
Config parameters
系统的参数配置
- API:显示 FreeRtos 调用的接口版本
- Version:显示 FreeRtos 的版本信息
- MPU/FPU:开启 MPU 或 FPU
- FPU:浮点运算单元,是专门运算浮点数的处理器,以前 FPU 是一种单独的芯片,之后集成在 CPU 中
- MPU:微处理器单元,就是把很多 CPU 集成在一起并行处理数据的芯片。但这里没有用到
- Kernel Setting:FreeRTOS 调度内核设置
- USE_PREEMPTION:是 RTOS 调度方式的选择
- 1:使用抢占式调度器,内核会在每个时钟节拍中断中进行任务切换
- 0:使用携程,会在某任务进入了函数
taskYIELD
,调用了使任务阻塞的 API 或者程序中定义了在中断中执行上下文切换这三种情况下进行任务切换
- CPU_CLOCK_HZ:CPU 时钟频率,默认使用晶振通过时钟树后获得的时钟频率
- TICK_RATE_HZ:是 RTOS 的心跳时钟频率,最大为 1000,也就是心跳时钟 1ms 跳动一次
- MAX_PRIORITIES:是 RTOS 任务的最高优先级设置,默认为 56。一般优先级表是 32 位,这里用了两个就是 64 位,其中的 8 位用于系统任务的优先级处理,任务优先级号越大,任务优先级越大
- MINIMAL_STACK_SIZE:设置分配给空闲任务的堆栈大小,这里使用字来指定,所以默认的 128 个子就是 128 * 4 个字节。这里可根据具体情况修改
- MAX_TASK_NAME_LEN:任务的名称,就是任务名字的字符串的大小,这里默认的 16 字节是足够的
- USE_16_BIT_TICKS:使用存放 Tick 周期的计数器数字位宽,默认为 Disable 就是 16 位
- IDLE_SHOULD_YIELD:任务是否应该调度切换
- 1:当有另一个空闲优先级任务处于 Ready 状态时,空闲任务将不会执行它定义的功能的不止一次迭代,而不会让位于另一个任务。这确保当应用程序任务处于空闲状态时,在空闲任务中花费的时间最少,即同在空闲优先级下,空闲任务优先级更高,不会被抢占,不会以时间片运行
- 0:空闲任务永远不会让位于另一个任务,只在被抢占时才会离开运行状态
- USE_MUTEXES:开启系统构建过程中的互斥量
- USE_RECURSIVE_MUTEXES:开启系统构建过程中的递归互斥量
- USE_COUNTING_SEMAPHORES:开启系统构建过程中的信号量
- QUEUE_REGISTRY_SIZE:队列注册表的大小,可以用于管理队列名称和队列实体,方便运行中进行查看与管理,默认为 8
- USE_APPLICATION_TASK_TAG:使能时给任务一个 TAG 标签,便于用户使用
- ENABLE_BACKWARD_COMPATIBILITY:一个兼容性使能,使能后 FreeRTOS 8.0.0 之后的版本可以通过宏定义使用 8.0.0 版本之前的函数接口,默认使能
- USE_PORT_OPTIMISED_TASK_SELECTION:查找下一个任务方式的选择,查找下一个就绪任务就是查找优先级表,对优先级表进行导 0 算法,分为通用切换或针对性切换
- 1:针对性切换使用处理器自带的导 0 指令,使用汇编编写,切换效率高,但兼容性差
- 0:默认,使用通用切换,使用 C 编写,执行效率低,但兼容性好
- USE_TICKLESS_IDLE:使能后生成两个空函数
PreSleepProcessing
和PostSleepProcessing
,用户可以编写代码进入低功耗模式 - USE_TASK_NOTIFICATIONS:任务通知使能,每个 RTOS 任务都有一个 32 位的通知值,RTOS 任务通知是一个直接发送给任务的事件,它可以解除接收任务的阻塞,并可选地更新接收任务的通知值
- 1:开启
- 0:关闭,可以为每个任务节省 8 个字节的内存空间
- RECORD_STACK_HIGH_ADDRESS:记录任务堆栈入口地址到 TCB,1 使能,0 关闭
- USE_PREEMPTION:是 RTOS 调度方式的选择
- Memory management setting:内存管理设置
- Memory Allocation:内存分配方式,此处是默认,动态和静态都可以
- TOTAL_HEAP_SIZE:内存堆的分配大小。堆本质就是一个数组,此处是设置堆数组的大小,设置时需要考虑最小要满足所有任务的使用要求,最大不能超过系统分配上限
- Memory Management scheme:内存分配方式,有 heap_1~heap_5 5 种。其中 1,2,4,5 都是先建立一个数组堆,从数组中申请,用完再释放,与 C 语言中
malloc
和free
使用链表的方式不一样,该方法在 MCU 中更安全稳定,此处默认使用的是 heap_4。具体申请和释放内存方式在 hea_4.c 中可看到
- Hool function related definitions:钩子函数配置。钩子函数是一种回调函数,用于在任务执行一次之后或者某些事件发生后执行的函数,该配置项里面有五个选项,控制5种不同功能的钩子函数开启,当然用户也可以在代码中自己定义
- USE_IDLE_HOOK:使能之后,系统生成一个空的回调函数
void vApplicationIdleHook(void)
,由用户编写函数主体。每当空闲任务执行一次,钩子函数被执行一次 - USE_TICK_HOOK:使能后,系统生成一个空回调函数
void vApplicationTickHook(void)
,由用户编写函数主体。每个 TICK 周期,钩子函数执行一次 - USE_MALLOC_FAILED_HOOK:使能之后,系统生成一个空的回调函数 **
void vApplicationMallocFailedHook(void)
,由用户编写函数主体,当申请动态内存失败时,钩子函数会执行一次 - USE_DAEMON_TASK_STARTUP_HOOK:使能之后,系统生成一个空回调函数
void vApplicationDaemonTaskStartupHook(void)
,任务刚启动时,钩子函数会执行一次 - CHECK_FOR_STACK_OVERFLOW:使能之后,系统生成一个空回调函数
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
,任务栈溢出时,钩子函数会执行一次,传入任务 TCB 和任务名称
- USE_IDLE_HOOK:使能之后,系统生成一个空的回调函数
- Run time and task stats gathering related definitions:任务运行跟踪配置
- GENERATE_RUN_TIME_STATS:开启时间统计功能,在调用
vTaskGetRunTimeStats()
函数时,将任务运行时间信息保存到可读列表中 - USE_TRACE_FACILITY:使能后会包含额外的结构成员和函数以帮助执行可视化和跟踪,默认开启,方便 MDK 软件工具调试使用
- USE_STATS_FORMATTING_FUNCTIONS:使能后会生成
vTaskList()
和vTaskGetRunTimeStats()
函数用于获取任务运行状态
- GENERATE_RUN_TIME_STATS:开启时间统计功能,在调用
- Co-routine related definitions:协程配置。
- USE_CO_ROUTINES:是否开启协程。开启之后需要用户手动创建协程,实际上协程很少用到了,而且 FreeRTOS 已经不会更新和维护协程了
- MAX_CO_ROUTINE_PRIORITIES:协程最大优先级
- Software timer definitions:软件定时器配置
- USE_TIMERS:默认开启软件定时器任务
- TIMER_TASK_PRIORITY:软件定时器任务优先级
- TIMER_QUEUE_LENGTH:定时器任务队列长度,FreeRTOS 是通过队列来发送控制命令给定时器任务,叫做定时器命令队列
- TMER_TASK_STACK_DEPTH:软件定时器任务堆栈大小
- Interrupt nesting behaviour configuration:中断优先级设置
- LIBRARY_LOWEST_INTERRUPT_PRIORITY:用来设置最低优先级的,FreeRTOS 使用的 4 位优先级,对应 16 位优先级,对应的最低优先级为 15
- LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:设置FreeRTOS 系统可管理的最大优先级,也就是设置阈值优先级,这个大家可以自由设置,这里设置为 5,也就是高于 5 的优先级(优先级数小于 5)不归 FreeRTOS 管理
Include Parameters
用于内核裁剪,裁剪掉不必要的功能,精简系统功能,减少资源占用,主要功能如下
- vTaskPrioritySet:改变某个任务的任务优先级
- uxTaskPriorityGet:查询某个任务的优先级。
- vTaskDelete:删除任务
- vTaskCleanUpResources:回收任务删除后的资源如RAM等等
- vTaskSuspend:挂起任务
- vTaskDelayUntil:阻塞延时一段绝对时间(绝对延时去去除程序执行时间,执行更精准)
- vTaskDelay:阻塞延时一段相对时间
- xTaskGetSchedulerState:获取任务调度器的状态,开启或未开启
- xTaskResumeFromISR:在中断服务函数中恢复一个任务的运行
- xQueueGetMutexHolder:获取信号量的队列拥有者,返回拥有此信号量的队列
- xSemaphoreGetMutexHolder:查询拥有互斥锁的任务,返回任务控制块
- pcTaskGetTaskName:获取任务名称
- uxTaskGetStackHighWaterMark:获取任务的堆栈的历史剩余最小值,FreeRTOS 中叫做“高水位线”
- xTaskGetCurrentTaskHandle:此函数用于获取当前任务的任务句柄,就是获取当前任务控制块
- eTaskGetState:此函数用于查询某个任务的运行壮态,比如:运行态、阻塞态、挂起态、就绪态等
- xEventGroupSetBitFromISR:在中断服务函数中将指定的事件位清零
- xTimerPendFunctionCall:定时器守护任务的回调函数(定时器守护任务使用到一个命令队列,只要向队列发送信号就可以执行相应代码,可以实现“中断推迟处理”功能)
- xTaskAbortDelay:中止延时函数,该函数能立即解除任务的阻塞状态,将任务插入就绪列表中
- xTaskGetHandle:此函数根据任务名字获取的任务句柄(控制块)
Tasks and Queues
任务
任务是操作系统运行的基本单元,也是资源分配的基本单元,直接在任务那里点击 Add 就可以添加新任务,任务的参数如下
- Task Name:任务名称,保存在 TCB 结构体中,设置时自己起名字
- Priority:任务优先级,任务的调度等级,根据自己创建任务的紧急程度设定。比如通信任务不能被打断,可以设计较高优先级
- Stack Size(Words):设定给任务分配的内存大小,单位是字,对于32位单片机来说占 4 个字节
- Entry Function:任务实体,即任务的运行函数名
- Code Generation:代码生成模式
- As weak:产生一个用 __weak 修饰的弱定义任务函数,用户可自己在进行定义;
- As external:产生一个外部引用的任务函数,用户需要自己定义该函数;
- Default:产生一个默认格式的任务函数,用户需要在该函数内实现自己功能
- Parameter:传入的参数,保持默认就行
- Allocation:内存分配方式
- Static:静态方式是直接在RAM占据一个静态空间
- Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间
设置完成之后即可直接生成代码,可以在 freertos.c
文件中找到任务的开启和定义
需要注意的是,任务必须是死循环
任务用户调用接口函数
osThreadNew
:创建新任务osThreadGetName
:获取任务名称osThreadGetId
:获取当前任务的控制块(TCB)osThreadGetState
:获取当前任务的运行状态osThreadGetStackSize
:获取任务的堆栈大小osThreadGetStackSpace
:获取任务剩余的堆栈大小osThreadSetPriority
:设定任务优先级osThreadGetPriority
:获取任务优先级osThreadYield
:切换控制权给下一个任务osThreadSuspend
:挂起任务osThreadResume
:恢复任务(挂起多少次恢复多少次)osThreadDetach
:分离任务,方便任务结束进行回收osThreadJoin
:等待指定的任务停止osThreadExit
:停止当前任务osThreadTerminate
:停止指定任务osThreadGetCount
:获取激活的任务数量osThreadEnumerate
:列举激活的任务
队列
队列,又称为消息队列,用于任务间的数据通信,传输数据,在操作系统里面,直接使用全局变量传输数据十分危险,看似正常运行,但不知道啥时候就会因为寄存器或者内存等等原因引起崩溃,所以引入消息,队列的概念,任务发送数据到队列,需要接受消息的任务挂起在队列的挂起列表,等待消息的到来。直接在 CubeMX 中点击 Add 创建队列,其中队列的参数如下
- Queue Name:队列名称(自己设定)
- Queue Size:消息队列大小
- Item Size:队列传输类型,保持默认16 位就行
- Allocation:队列内存的分配方式
- Static:静态方式是直接在RAM占据一个静态空间
- Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间
然后点击生成代码,之后就可在 freertos.c
中系统初始化函数中看到队列的初始化
初始化函数会在一开始被调用,对 FreeRTOS 系统和内核对象进行初始化,初始化后系统就可以进行调度和使用内核对象,CubeMX 生成的代码自动将创建的内核对象放到初始化函数内,所以在任务和中断中直接使用就可以。
队列用户调用函数接口
osMessageQueueNew
:创建并初始化一个新的队列osMessageQueueGetName
:获取队列的名字osMessageQueuePut
:发送一条消息到队列osStatus_t osMessageQueuePut (osMessageQueueId_t mq_id, const void *msg_ptr, uint8_t msg_prio, uint32_t timeout)
mq_id
:传入队列的句柄msg_ptr
:指向需要发送的消息内容的指针msg_prio
:本次发送消息的优先级,但是实际中并没有用到timeout
:发送消息的超时时间,设置为 0 表示一直等待发送成功osStatus_t
:返回执行结果osOK
:执行正常osError
:系统错误osErrorTimeout
:执行超时osErrorResource
:资源不可用osErrorParameter
:参数无效osErrorNoMemory
:内存不足osErrorISR
:不允许在中断调用osStatusReserved
:防止编译器优化项,不需要在意
osMessageQueueGet
:从队列等待一条消息osStatus_t osMessageQueueGet (osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout)
mq_id
:接受队列的句柄msg_ptr
:用于接受消息内容的指针msg_prio
:存放接受消息的优先级(目前API未加入功能)timeout
:接受消息的超时时间(设置为10代表,当前任务挂起在挂起列表,直到接收成功时恢复,或者 10 个 TICK 等待周期到达然后任务强行恢复,不再等待,为 0 则是不等待,等待期间任务挂起在内核对象的挂起队列)osStatus_t
:返回执行结果,同上
osMessageQueueGetCapacity
:获取队列传输消息的峰值osMessageQueueGetMsgSize
:获取队列使用内存池的最大峰值osMessageQueueGetCount
:获取队列的消息数量osMessageQueueGetSpace
:获取队列剩余的可用空槽osMessageQueueReset
:清空队列osMessageQueueDelete
:删除队列
Timers and Semaphores
创建定时器和信号量
创建定时器
软件定时器本质上就是设置一段时间,当设置的时间到达之后就执行指定的回调函数,回调函数的两次执行间隔叫做定时器的定时周期
在 CubeMX 中直接在 Timers 中点击 Add 即可添加定时器,具体参数如下
- Timer Name:设置定时器的名称
- Callback:设定定时器的回调函数体
- Type:设定定时器的执行类型
- osTimerPeriodic:定时器周期执行回调函数
- osTimerOnce:定时器只执行一次回调函数
- Code Generation Option:代码生成模式
- As weak:产生一个用 __weak 修饰的弱定义任务函数,用户可自己在进行定义
- As external:产生一个外部引用的任务函数,用户需要自己定义该函数
- Default:产生一个默认格式的任务函数,用户需要在该函数内实现自己的功能
- Parameter:传入参数,保持默认 NULL 就行
- Allocation:软件定时器内存的分配方式,一般使用动态
- Static:静态方式是直接在RAM占据一个静态空间
- Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间
之后直接生成代码即可在 freertos.c
文件中看到定时器创建之后获得的句柄和生成的回调函数
之后就是 CubeMX 提供的定时器接口函数以及功能
osTimerNew
:新建定时器,返回定时器控制句柄osTimerGetName
:获取定时器名称osTimerStart
:设置定时器周期,启动定时器osStatus_t osTimerStart (osTimerId_t timer_id, uint32_t ticks)
timer_id
需要启动的定时器句柄ticks
设置定时器的运行周期
osTimerStop
:停止定时器osStatus_t osTimerStop (osTimerId_t timer_id)
timer_id
需要启动的定时器句柄
osTimerIsRunning
:检测定时器是否在运行osTimerDelete
:删除定时器
软件定时器是由软件定时器维护任务进行维护的,检测各个定时器的状态,进行处理
创建信号量
信号量是 RTOS 的一个内核对象,该对象有一个队列表示该信号量拥有的信号数目,任何任务都可以对这个信号数目进行获取和释放,获取时信号 - 1,释放时信号 + 1,为 0 时不能继续获取,此时有任务想要继续获取信号量的话,任务会挂起在该内核对象的挂起列表,等到信号可以获取时进行恢复,根据这个特性,信号量常用于控制对共享资源的访问和任务同步
在配置界面中可以看到有两个信号量添加页面
- Binary Semaphores 是二值信号量,仅有一个 token,用于同步一个操作
- Counting Semaphores 是计数信号量,有多个 token,可用于同步多个操作
在对应的信号量中点击 Add 即可创建信号量,可以看到初始的一些配置
Binary Semaphores 配置
- Semaphore Name:信号量名称
- Allocation:内存分配方式,一般使用动态
- Static:静态方式是直接在RAM占据一个静态空间
- Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间
Counting Semaphore 配置
- Semaphore Name:信号量名称
- Count:计数信号量的最大数目
- Allocation:内存分配方式,一般使用动态
- Static:静态方式是直接在 RAM 占据一个静态空间
- Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间
之后直接生成代码即可在 freertos.c
文件中看到信号量创建之后获得的句柄和生成的回调函数
之后就是 CubeMX 提供的信号量操作接口函数以及功能
osSemaphoreNew
:创建新的信号量osSemaphoreGetName
:获取信号量的名称osSemaphoreAcquire
:获取信号量osStatus_t osSemaphoreAcquire (osSemaphoreId_t semaphore_id, uint32_t timeout)
semaphore_id
:传入要获取信号量的控制句柄timeout
:获取等待时间(等待期间任务挂起在内核对象的挂起队列)osStatus_t
:返回osOK
即为获取成功
osSemaphoreRelease
:释放信号量osStatus_t osSemaphoreRelease (osSemaphoreId_t semaphore_id)
semaphore_id
:传入要释放的信号量控制句柄osStatus_t
:返回osOK
即为释放成功
osSemaphoreGetCount
:获取当前可用信号量的数目osSemaphoreDelete
:删除信号量
二值信号量和计数信号量的操作基本一致,没用区别,只是用有的信号队列最大数目不同而已。同时注意信号量在使用过程中会出现优先级反转的 Bug,使用时需要注意
Mutexs
互斥量其实就是一个拥有优先级继承的二值信号量,互斥信号量适合用于那些需要互斥访问的应用中,在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源,与信号量不同的是,互斥量的释放必须由获取他的任务进行释放,如果不释放,可能会造成死锁
CubeMX 中提供了两种信号量
- Mutexs:普通信号量,只能获取一次,重复获取无效
- Recursive Mutexs:递归信号量,可以获取多次,同样也需要释放多次才能让出使用权
直接点击 Add 然后配置参数,如下
- Mutex Name:互斥量名称
- Allocation:内存分配方式,一般使用动态
- Static:静态方式是直接在RAM占据一个静态空间
- Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间
两种信号量配置方式一样,参数也一样,只是在用法上不同。生成代码之后,会在 freertos.c
文件中看到互斥量初始化完成,并且生成了对应的控制句柄
CubeMX 提供了信号量的使用接口,如下
osMutexNew
:创建互斥量osMutexGetName
:获取互斥量名称osMutexAcquire
:任务获取互斥量osStatus_t osMutexAcquire (osMutexId_t mutex_id, uint32_t timeout)
mutex_id
:互斥量控制句柄timeout
:获取互斥量时的等待时间(等待期间任务挂起在内核对象的挂起队列)osStatus_t
:返回osOK
即为获取成功
osMutexRelease
:任务释放互斥量osStatus_t osMutexRelease (osMutexId_t mutex_id)
mutex_id
:互斥量控制句柄osStatus_t
:返回osOK
即为释放成功
osMutexGetOwner
:获取互斥量的拥有任务的任务 TCBosMutexDelete
:删除互斥量
Events
任务间的同步除了信号量还有时间标志组,信号的同步通常是一对一的同步,有的时候系统需要多对一的同步,比如同时满足5个按键按下时,任务启动,如果使用信号会很占据资源,所以 RTOS 引入了事件标志组来满足这一需求
在 CubeMX 中直接点击 Add 创建事件标志组,其中参数如下
- Event flags Name:事件标志组名称
- Allocation:内存分配方式,一般使用动态
- Static:静态方式是直接在 RAM 占据一个静态空间
- Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间
配置完成后,生成代码,在系统初始化内,可以看到生成事件标志组控制句柄
CubeMX 提供的事件操作接口如下
osEventFlagsNew
:创建事件标志组osEventFlagsGetName
:获取事件标志组名称osEventFlagsSet
:设置事件标志组uint32_t osEventFlagsSet (osEventFlagsId_t ef_id, uint32_t flags)
ef_id
:事件标志组控制句柄flags
:事件位
osEventFlagsClear
:清除事件标志组osEventFlagsGet
:获取当前事件组标志信息osEventFlagsWait
:等待事件标志组触发uint32_t osEventFlagsWait (osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout)
ef_id
:事件标志组控制句柄flags
:等待的事件位options
:等待事件位的操作osFlagsWaitAny
:等待的事件位有任意一个等到就恢复任务osFlagsWaitAll
:等待的事件位全部等到才恢复任务osFlagsNoClear
:等待成功后不清除所等待的标志位(默认清除)
timeout
:等待事件组的等待时间(等待期间任务挂起在内核对象的挂起队列)
osEventFlagsDelete
:删除事件标志组
User Constants
用户常量,将不变的量转化为常量保存,可以节省 RAM 资源空间,因为常量和变量的保存位置不同
任务通知
FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue 就是这个通知值。任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞的话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态,CubeMX内没有提供相关的配置项,但在其生成的 FreeRTOS 接口里面有相关函数进行配置,函数声明位于 cmsis_os2.h
中
函数接口如下
osThreadFlagsSet
:设置任务的通知标志uint32_t osThreadFlagsSet (osThreadId_t thread_id, uint32_t flags)
thread_id
:任务控制块flags
:设置的标志
osThreadFlagsClear
:清除任务通知osThreadFlagsGet
:获取任务标志osThreadFlagsWait
:等待特定的任务标志uint32_t osThreadFlagsWait (uint32_t flags, uint32_t options, uint32_t timeout)
flags
:设置的标志options
:设置功能osFlagsWaitAny
:等待的事件位有任意一个等到就恢复任务osFlagsWaitAll
:等待的事件位全部等到才恢复任务osFlagsNoClear
:等待成功后不清除所等待的标志位(默认清除)
timeout
:超时时间
任务通知其实个任务事件标志组使用上没有多大的区别,但他们两个的实现原理不同,同时任务通知对资源的占用更少。根据 FreeRTOS 官方的统计,使用任务通知替代二值信号量的时候任务解除阻塞的时间要快 45%,并且需要的 RAM 也更少
系统内核配置
CubeMX 生成的代码中封装了一系列内核配置函数,如下
osKernelInitialize
:初始化RTOS的内核osKernelGetInfo
:获取RTOS的信息osKernelGetState
:获取当前内核的运行状态osKernelStart
:启动内核调度osKernelLock
:锁内核调度器osKernelUnlock
:解锁内核调度器osKernelRestoreLock
:恢复RTOS内核调度器锁状态osKernelSuspend
:挂起任务osKernelResume
:恢复任务osKernelGetTickCount
:用于获取系统当前运行的时钟节拍数osKernelGetTickFreq
:用于获取系统当前运行的时钟节拍的分频频率osKernelGetSysTimerCount
:获取系统时钟SysTick
的计数值osKernelGetSysTimerFreq
:获取系统时钟SysTick
的频率
FreeRTOS 特点
- 实时性:FreeRTOS可以配置成为一个硬实时操作系统内核,也可以配置为非实时型内核,甚至于部分任务是实时性的,部分不是
- 任务数量:FreeRTOS对任务数没有限制,同一优先级也可以有多个任务
- 抢占式或协作式调度算法:任务调度既可以为抢占式也可以为协作式。采用协作式调度算法后,一个处于运行态任务除非主动要求任务切换,否则是不会被调度出运行态的
- 任务调度的时间点:调度器会在每次定时中断到来时决定任务调度,同时外部异步事件也会引起调度器任务调度
- 调度算法:任务调度算法首先满足高优先级任务最先执行,当多于 1 个任务具有相同的高优先级时,采用 round robin 算法调度
- 优先级翻转:FreeRTOS 没有提供优先级继承机制或其他的避免优先级翻转的方法
- 任务间通信:FreeRTOS 支持队列和几种基本的任务同步机制
- 消息队列:任务间传递信息可以采用队列方式,FreeRTOS实现的队列机制传递信息是采用传值方式,因此对于传递大量数据效率有些低。但可以通过传递指针的方式提高效率。中断处理函数中读写队列都是非阻塞型的。任务中读写队列可以为阻塞型也可以配置非阻塞型。当配置为阻塞型时可以指定一个阻塞的最大时间限(Timeout)。
- 任务间同步:FreeRTOS 支持基本的信号量功能。FreeRTOS 采用队列来实现信号量的功能,可以认为一个值为n的信号量就是一个长度为n的队列,队列中每个元素的大小为0。这样的队列并不会浪费宝贵的内存空间。
- 对于死锁(Deadlock)的处理: FreeRTOS 并没有实现一种可以完全避免死锁的机制。只是通过指定一个阻塞的最大时间限(Timeout)来减少死锁现象的发生。或者说是给出了当死锁现象发生时解锁的可能。当然能不能真的解锁要依赖于使用者的处理代码是否合适。
- 临界区:FreeRTOS 采用开关中断的方式实现临界区保护。任务代码中临界区可以嵌套,FreeRTOS 会自动记录每个任务中临界区嵌套的层数。
- 暂停调度:与进入临界区类似,FreeRTOS 可以通过暂时关闭任务调度来保证任务代码不被更高优先级的其他任务打断,与临界区不同,关闭任务调度并不会关闭中断,这样中断处理函数仍会照常的执行。
- 内存分配:FreeRTOS 提供了多种内存动态分配的方法,具体程序中需要选择其中一种。最简单的内存分配方式提供了一种非常简单的固定内存分配算法,这种方式下只支持内存的分配,不支持分配内存的回收。因此,任务建立后就不能被删除。其他几种内存分配算法支持分配内存的回收,有的方法支持邻接内存块的合并,有些不支持。对于 uCOS-II 中内存分配的方法,既保证了实时性,也具有一定的灵活性。FreeRTOS 中提供的几种方式,实时性好的功能上有缺陷,功能上完善的实时性却不好
FreeRTOS 任务调度机制
调度策略
- 可抢占:在可抢占式调度中,任务可以被更高优先级的任务抢占。当一个高优先级任务变得可用时,它可以打断当前正在执行的低优先级任务,从而使系统立即切换到高优先级任务执行,无论被抢占的任务是否已经执行完其时间片。它确保了高优先级任务能够及时响应,并在需要时立即执行,不受低优先级任务的阻碍。通过设置
configUSE_PREEMPTION
来决定是否启动抢占 - 时间片轮转:操作系统为每个任务分配一个时间片,即预定义的时间量。在时间片轮转调度方式下,每个任务可以执行一个时间片,然后系统将控制权移交给下一个就绪的任务。如果一个任务在其时间片结束前没有完成,系统会暂停该任务,将控制权交给下一个就绪的任务。时间片的大小可以根据应用程序的需要进行调整。这种调度方式有助于确保任务之间的公平性,避免某些任务长时间占用处理器,同时允许多个任务分享处理时间。通过设置
configUSE_TIME_SLICING
来决定是否启用时间片轮转 - 组合应用:可抢占和时间片轮转调度方式可以结合使用。这样可以实现灵活的任务管理,确保高优先级任务能够抢占低优先级任务,并且为任务提供公平的处理器时间,从而有效地管理系统资源
实现核心
任务管理使用就绪链表,阻塞链表和挂起链表来管理任务的状态和调度。这些链表用于维护不同状态的任务列表
- 就绪链表:就绪链表包含所有处于就绪状态的任务。就绪状态的任务就是已经准备好运行,但由于当前执行的任务正在占用 CPU 资源,它们暂时无法立即执行。这些任务按照优先级被组织在就绪链表中。当当前正在执行的任务释放 CPU(例如,由于时间片用完,任务阻塞或挂起等原因)时,调度器从就绪链表中选择优先级最高的任务来执行
- 阻塞链表:阻塞链表包含那些由于某种原因而无法立即执行的任务。这些原因可能包括等待某个事件、资源不可用、延时等情况。当任务处于阻塞状态时,它们不会被调度器所执行。这些任务会在特定条件满足之后重新放入就绪链表,等待调度器选择其执行
- 挂起链表:挂起链表包含已被显式挂起的任务。当任务被挂起时,它们暂时停止运行,不再参与调度。这些任务不会出现在就绪链表或阻塞链表中,因为它们被明确地挂起,不参与任务调度。
这些链表是 FreeRTOS 内部任务管理的一部分,并且开发者可以通过 FreeRTOS 提供的 API 函数来管理和操作任务的状态以及链表中的任务
FreeRTOS 任务释放 CPU 的几种情况
- 任务主动让出CPU:任务可以调用
osDelay
函数或者osDelayUntil
函数,将自己挂起一段时间,以便其他任务能够运行。这种方式是任务主动放弃CPU的一种方式。 - 阻塞等待事件:任务可以调用 FreeRTOS 提供的阻塞函数,如
osMessageQueueGet
和osSemaphoreAcquire
等,来等待特定事件的发生。当任务在等待某个事件时,它会被置于阻塞状态,从而释放 CPU,直到事件发生后才会被唤醒。 - 时间片轮转:如果使用了时间片轮转调度策略,任务会在其时间片用尽时自动释放CPU,允许其他任务运行。时间片轮转是一种公平分配CPU时间的策略,每个任务都有一个小的时间片来执行,然后被放回就绪队列,等待下一次执行。
- 任务进入阻塞状态:任务在执行过程中,如果发生某些阻塞事件,如等待一个队列满足条件和等待互斥信号量等,会自动进入阻塞状态,这时会释放 CPU。一旦阻塞条件得到满足,任务将被重新置于就绪状态。
FreeRTOS 使能之后黄色感叹号
When FreeRTOS is used, it is strongly recommanded to use HAL timebase source other than the Systick
HAL函数如果是阻塞型呼叫,內部会用到 HAL_Delay()
,FreeRTOS应该还是使用 SystTick
。如果使用的时基操作來源一样,怕有不可预期问题出现,故选择其他定时器
FreeRTOS 任务只能启动几个
这是因为 FreeRTOS 配置中的 TOTAL_HEAP_SIZE 设置的有点小,导致给栈分配的内存不够了,以至于分配失败而任务也不能运行
外部中断
外部中断就是 GPOIO 引脚对外部的输入信号进行检测,对应的上升/下降沿会触发中断(具体看设置情况)
寄存器
外部中断的功能可以配置以下六个寄存器:
- 中断屏蔽寄存器(EXTI_IMR)
- 事件屏蔽寄存器(EXTI_EMR)
- 上升沿触发选择寄存器(EXTI_RTSR)
- 下降沿触发选择寄存器(EXTI_FTSR)
- 软件中断事件寄存器(EXTI_SWIER)
- 挂起寄存器(EXTI_PR)
EXTI
EXTI 支持配置 20 个 中断和事件屏蔽位:
EXTI_Line0~EXTI_Line15
:GPIO 对应的引脚所触发的中断EXTI_Line16
:连接到 PVD 输出EXTI_Line17
:连接到 RTC 闹钟事件EXTI_Line18
:连接到 USB 唤醒事件EXTI_Line19
:连接到以太网唤醒事件,只适用于互联网型产品
中断服务函数的映射关系
GPIO | IRQN | IRQN_Handler |
---|---|---|
GPIO_Pin0 | EXTI0_IRQn | EXTI0_IRQHandler |
GPIO_Pin1 | EXTI1_IRQn | EXTI1_IRQHandler |
GPIO_Pin2 | EXTI2_IRQn | EXTI2_IRQHandler |
GPIO_Pin3 | EXTI3_IRQn | EXTI3_IRQHandler |
GPIO_Pin4 | EXTI4_IRQn | EXTI4_IRQHandler |
GPIO_Pin5 — GPIO_Pin9 | EXTI9_5_IRQn | EXTI9_5_IRQHandler |
GPIO_Pin10 — GPIO_Pin15 | EXTI15_10_IRQn | EXTI15_10_IRQHandler |
配置
首先是在右侧选中引脚,将其设置为 GPIO_EXTI0
模式,然后左侧开始进行配置
GPIO 配置
- GPIO mode:触发中断的模式
- External Interrupt Mode with Rising edge trigger detection:外部中断模式上升沿触发
- External Interrupt Mode with Falling edge trigger detection:外部中断模式下降沿触发
- External Interrupt Mode with Rising/Falling edge trigger detection:外部中断模式上升和下降沿触发
- External Event Mode with Rising edge trigger detection:外部事件模式上升沿触发
- External Event Mode with Falling edge trigger detection:外部事件模式下降沿触发
- External Event Mode with Rising/Falling edge trigger detection:外部事件模式上升和下降沿触发
- GPIO Pull-up/Pull-down:GPIO 的上拉下拉模式
- User Label:用户别名
中断配置
这里所用到的中断就是 EXTI0_IQRHandler
中断函数。从 stm32f4xx_it.c
中找到 HAL 库中的函数,然后可以通过实现 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
函数来实现中断回调函数
SPI
介绍
SPI 就是 Serial Peripheral interface 的缩写,就是串行外围设备接口。它是一种高速的,全双工的,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚
SPI 是全双工且 SPI 没有速度限制,一般的实现通常能达到甚至超过 10 Mbps
主从模式
SPI 分为主,从两种模式。一个 SPI 通信系统需要包含一个主设备和一个或多个从设备。其中提供时钟的是主设备,接收时钟的设备为从设备。SPI 接口的读写操作都是由主设备发起的。当存在多个从设备时,通过各自的片选信号进行管理
硬件信号线
SPI 接口一般使用四条信号线通信:SDI 数据输入,SDO 数据输出,SCK 时钟,CS 片选。如下
MISO
主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据MOSI
主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据SCLK
串行时钟信号,由主设备产生CS/SS
从设备片选信号,由主设备控制。它的功能是用来作为片选引脚,也就是选择指定的从设备。可以让主设备单独地与特定从设备通信,避免总线上的冲突
发送接收原理
- 通讯开始:NSS 线信号由高变低,对应的从机被选中,开始通讯
- 通讯结束:NSS 线信号由低变高,对应的从机取消被选中状态
SPI 主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输
- 首先拉低对应的 SS 信号线,表示与该设备进行通信
- 主机通过发送 SCLK 时钟信号来告诉从机写数据或读数据
- 主机将要发送的数据写入到发送数据缓存区,缓存区经过移位寄存器,串行移位寄存器通过 MOSI 信号线将字节一位一位的传送给从机,同时 MISO 接口接收到的数据经过移位寄存器移动到接收缓冲区
- 从机也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。同时通过 MOSI 信号线接收主机发送的数据,以此来实现交换两个移位寄存器中的内容
SPI 只有主从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需要忽略接收到的字节即可。如果主机想要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是发送数据时必然会收到一个数据,想要接收到一个数据就必须先发送一个数据
工作模式
根据 SPI 的时钟极性 CPOL 和相位 CPHA 不同,SPI 有四种工作模式
时钟极性定义了时钟空闲时电平
- CPOL=0:时钟空闲时为低电平
- CPOL=1:时钟空闲时为高电平
时钟相位定义数据的采集事件
- CPHA=0:在时钟的第一个跳变沿(上升沿或者下降沿)进行数据采样
- CPHA=1:在时钟的第二个跳变沿进行数据采样
上面经过组合之后可以得到,但是需要注意的是,主从机必须选用同一种模式
SPI 模式 | CPOL | CPHA | 空闲时 SCK 时钟 | 采样时刻 |
---|---|---|---|---|
0 | 0 | 0 | 低电平 | 第一个边沿(奇) |
1 | 0 | 1 | 低电平 | 第二个边沿(偶) |
2 | 1 | 0 | 高电平 | 第一个边沿(奇) |
3 | 1 | 1 | 高电平 | 第二个边沿(偶) |
SPI 工作在 3 种模式下,分别是运行,等待和停止
- 运行模式(Run Mode)这是基本的操作模式
- 等待模式(Wait Mode)SPI 工作在等待模式是一种可配置的低功耗模式,可以通过 SPICR2 寄存器的 SPISWAI 位进行控制。在等待模式下,如果 SPISWAI 位清 0,SPI 操作类似于运行模式。如果 SPISWAI 位置 1,SPI 进入低功耗状态,并且 SPI 时钟将关闭。如果 SPI 配置为主机,所有的传输将停止,但是会在 CPU 进入运行模式后重新开始。如果 SPI 配置为从机,会继续接收和传输一个字节,这样就保证从机与主机同步
- 停止模式(Stop Mode)为了降低功耗,SPI 在停止模式是不活跃的。如果 SPI 配置为主机,正在进行的传输会停止,但是在 CPU 进入运行模式后会重新开始。如果 SPI 配置为从机,会继续接受和发送一个字节,这样就保证了从机与主机同步
配置
这里使用大疆C板做配置。将 SPI 模式设置为全双工主模式,其他模式为
- Full-Duplex Master:全双工主模式
- Full-Duplex Slave:全双工从模式
- Half-Duplex Master:半双工主模式
- Half-Duplex Slave:半双工从模式
- Receive Only Master:只接收主模式
- Receive Only Slave:只接收从模式
- Transmit Only Master:只发送主模式
- Transmit Only Slave:只发送从模式
在 STM32 上有硬件 NSS 片选信号,可以选择使能,也可以使用其他 IO 口接到芯片的 NSS 上进行代替。其中
- SPI1 的片选 NSS 为 PA4
- SPI2 的片选 NSS 为 PB12
如果片选引脚没有连接这两个引脚,则需要选择软件片选
对应的软件片选,需要设置对应的引脚,比如这里就是选择 PA4 和 PB0 作为软件片选引脚,将其设置为输出模式
NSS 管脚以及片选信号:
作为主设备 NSS 管脚为高电平,从设备 NSS 管脚为低电平。当 NSS 管脚为低电平时,该 SPI 设备被选中,可以和主设备进行通信
在 STM32 中,每个 SPI 控制器的 NSS 信号引脚都具有两种功能,即输入和输出
- 输入:就是 NSS 管脚的信号给自己
- 输出:就是将 NSS 的信号送出去给从机
对于NSS的输入,又分为软件输入和硬件输入。
- 软件输入:NSS 分为内部管脚和外部管脚,通过设置 spi_cr1 寄存器的 ssm 位和 ssi 位都为 1 可以设置 NSS 管脚为软件输入模式且内部管脚提供的电平为高电平,其中 SSM 位为使能软件输入位。SSI 位为设置内部管脚电平位。同理通过设置 SSM 和 SSI 位分别为 1 和 0,则此时的 NSS 管脚为软件输入模式但内部管脚提供的电平为 0。若从设备是一个其他的带有 SPI 接口的芯片,并不能选择 NSS 管脚的方式,则可以有两种办法,一种是将 NSS 管脚直接接低电平,另一种就是通过主设备的任何一个 GPIO 口去输出低电平选中从设备
- 硬件输入:主机接高电平,从机接低电平
这里对 SPI 的参数设置如下
- Frame Format:框架格式
- Motorola
- TI
- Data Size:数据长度
- 8 Bit
- 16 Bit
- First Bit:对齐格式
- MSB:高位先行
- LSB:低位先行
- Prescaler:预分频,用于控制波特率
- Baud Rate:波特率=16MHz/Prescaler
- Clock Polarity:CPOL
- Clock Phase:CPHA
- CRC Calculation:是否启用 CRC 校验
- NSS Signal Type:片选形式,硬件实现还是软件实现
在 NVIC Settings 中启用 SPI global interrupt,这就打开了 SPI 的中断
然后在 DMA Settings 中添加 DMA 的接收和发送数据流,如上。其中有个 DMA 接收参数
- DMA 模式,有两种模式,这里与 UART 是一致的
- Normal:只进行一次接收或者发送
- Circular:进行多次接收或者发送
配置 DMA 时一定要将 SPI 的中断打开
三种接收和发送
阻塞式
1 | // 发送数据 |
中断式
1 | // 发送数据 |
DMA 式
1 | // DMA 发送数据 |
IIC
简介
IIC 总线是一种由飞利浦公司开发的串行总线。是两条串行的总线,由一根数据线和一根时钟线组成。IIC 上可以连接多个设备,每个器件都有一个唯一的地址识别。同一事件只能有一个主设备,其他的为从设备
STM32 的 IIC 外设可用作通讯的主机以及从机,支持 100Kbs 和 400Kbs 的传输速率,支持 DMA 传输数据,并且具有数据校验功能。需要注意的是 IIC 是为了与低速设备通信而发明的,所以传输速率上比不上 SPI
硬件物理层
IIC 一共有两条线,一条是双向的数据线 SDA,一条是串行的时钟线 SCL
所有接到 IIC 总线设备上的串行数据线 SDA 都接在总线的 SDA 上,所有设备的时钟线 SCL 接到总线的 SCL 上。IIC 总线上的每一个设备都对应着唯一的地址
传输的起始信号和停止信号
- 起始信号:SCL 保持高电平,SDA 由高电平转为低电平之后,延时 SCL 变为低电平
- 停止信号:SCL 保持高电平,SDA 由低电平变为高电平
传输数据的有效性
IIC 信号在传输过程中,当 SCL=1 高电平时,数据线 SDA 必须保持稳定状态,不允许由电平跳变,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。SCL=1 时,数据线 SDA 的任何电平变换都会被看作是总线的起始或者停止信号。也就是在 IIC 传输数据的过程中,SCL 时钟线会频繁的转换电平,以保证数据的传输
传输应答信号
每当主机向从机发送完一个字节的数据时,主机都需要等待从机给出一个应答信号以确认从机是否接收到了数据
应答信号:主机 SCL 拉高,读取从机 SDA 电平,为低电平表示产生应答
- 当应答信号为低电平时,规定为有效应答位,表示接收器已经成功接收到了该字节
- 当应答信号为高电平时,规定为非应答位,一般表示接收器接收该字节没有成功
应答出现在每一次主机完成一个字节的数据传输之后紧跟着的时钟周期,低电平表示应答,高电平表示非应答
模式选择
IIC 在工作室可以选用以下四种模式之一
- 从发送器
- 从接收器
- 主发送器
- 主接收器
默认情况下,设备以从模式工作。接口在生成起始位后会自动由从模式切换为主模式,并在出现仲裁丢失或生成停止位时从主模式切换为从模式,从而实现多主模式功能
通信流程
在主模式下,IIC 接口会启动数据传输并生成时钟信号。串行数据传输始终是在出现起始信号时开始,在出现停止信号时结束。起始位和停止位均在主模式下由软件生成
在从模式下,该接口能够识别其自身地址(7 或 10 位)以及广播呼叫地址。广播呼叫地址检测可由软件使能或禁止
数据和地址均以 8 位字节传输,MSB 在前。起始信号后紧紧跟随这地址字节,7 位地址占据一个字节,10 位地址占据两个字节。地址始终在主模式下传送
在传输 8 个时钟周期之后的第 9 个时钟脉冲期间中,接收器必须向发送器发送一个应答信号。应答信号可被软件使能或禁止
从模式
默认情况下,IIC 接口在从模式下工作,要将工作模式由默认的从模式切换为主模式,需要生成一个起始位
为了时序正确,必须在 IIC_CR2
寄存器中对外设输入时钟进行编程。外设时钟的频率下限为
- 标准模式:2 MHz
- 快速模式:4 MHz
从模式工作流程
- 检测到起始信号之后,便会立即接收到 SDA 线上的地址,并将其存放在移位寄存器中。之后会将其与接口地址或者广播呼叫地址比较(10 位地址模式下需要包含头序列 11110xx0,其中 xx 表示该地址的两个最高有效位)
- 头或地址不匹配:接口会忽略并且等待下一个起始信号
- 头匹配,只针对 10 位地址模式:如果 ACK 位置位,则接口会生成一个应答脉冲并且等待 8 位从地址
- 地址匹配
- 发送应答脉冲
- ADDR 位会由硬件置位,并且在 ITEVFEN 位置位时生成一个中断
- 如果 ENDUAL=1,则软件必须读取 DUALF 位的状态来核对那些从地址进行了应答
- 在 10 位模式下,完成地址序列接收之后,从模式时钟处于接收模式。在接收到重复起始信号以及一个匹配地址位和最低有效位置位的头序列(11110xx1)之后,会进入发送模式
- TRA 位指示从设备是处于接收模式还是处于发送模式
从发送器
- 在接收到地址并且将 ADDR 清零之后,从设备会通过内部移位寄存器将 DR 寄存器中的字节发送到 SDA 线上
- 从设备会延长 SCL 低电平时间,直到 ADDR 位清零并且 DR 寄存器中填满待发送的数据为止。然后进行数据的发送
- 接收到应答脉冲时:TxE 位会由硬件置 1 并在 ITEVFEN 和 ITBUFEN 位均置 1 时生成一个中断
- 如果在下一次数据传输结束之前 TxE 位已置 1 但某些数据尚未写入 IIC_DR 寄存器,则BTF 位会置 1,而接口会一直延长 SCL 低电平,直到通过软件对 IIC_SR1 读操作,以及对
IIC_DR
写操作后,把 BTF 清零为止
从接收器
- 在接收到地址并将 ADDR 位清零后,从设备会通过内部移位寄存器接收 SDA 线中的字节并将其保存到 DR 寄存器。在每个字节接收完成后
- 发出应答脉冲
- RxNE 位会由硬件置 1 并在 ITEVFEN 和 ITBUFEN 位均置 1 时生成一个中断
- 如果在下一次数据接收结束之前 RxNE 位已置 1 但 DR 寄存器中的数据尚未读取,则 BTF 位会置 1,而接口会一直延长 SCL 低电平,直到软件通过读取
IIC_DR
寄存器来把 BTF 清零
关闭从设备通信
传输完最后一个数据字节之后,主设备会生成一个停止位。接口会检测此条件并将 STOPF 位置 1 并在 ITEVFEN 位置 1 时生成一个中断
通过先读取 SR1 寄存器然后写入 CR1 寄存器的方式将 STOPF 位清零
主模式
在主模式下,IIC 接口会启动数据传输并生成时钟信号。串行数据传输始终是在出现起始信号时开始,在出现停止信号时结束。只要通过 START 位在总线上生成了起始信号,即会选中主模式
在主模式下需要
- 在
IIC_CR2
寄存器中对外设输入时钟进行编程,以生成正确的时序 - 配置时钟控制寄存器
- 配置上升时间寄存器
- 对
IIC_CR1
寄存器进行编程以便使能外设 - 将
IIC_CR1
寄存器的 START 置位以生成起始信号 - START 置位之后,接口会在 BUSY 位清零之后生成一个起始位并切换到主模式。在主设备下的接口会在当前字节传输结束后生成一个重复起始位
- 接下来从地址会通过内部移位寄存器发送到 SDA 线
- 10 位寻址模式下:发送头序列,然后发送从地址,再次发送头序列来决定进入发送模式还是接收模式,最低有效位置位进入接收模式
- 7 位寻址模式下:发送一个地址字节,最低有效位决定进入发送模式还是接收模式,最低有效位置位进入接收模式
主发送器
- 在发送出地址并将 ADDR 清零后,主设备会通过内部移位寄存器将 DR 寄存器中的字节发送到 SDA 线
- 主设备会一直等待,直到首个数据字节被写入
IIC_DR
为止 - 接收到应答脉冲后,TxE 位会由硬件置 1 并在 ITEVFEN 和 ITBUFEN 位均置 1 时生成一个中断
- 如果在上一次数据传输结束之前 TxE 位已置 1 但数据字节尚未写入 DR 寄存器,则 BTF 位会置 1,而接口会一直延长 SCL 低电平,等待IIC_DR 寄存器被写入,以将 BTF 清零
- 通信结束:当最后一个字节写入 DR 寄存器后,软件会将 STOP 位置 1 以生成一个停止信号,接口会自动返回从模式(M/SL 位清零)
主接收器
- 完成地址传输并将 ADDR 位清零后,IIC 接口会进入主接收模式。在此模式下,接口会通过
- 内部移位寄存器接收 SDA 线中的字节并将其保存到 DR 寄存器。在每个字节传输结束后,接口都会依次:
- 发出应答脉冲(如果 ACK 位置 1)
- RxNE 位置 1 并在 ITEVFEN 和 ITBUFEN 位均置 1 时生成一个中断
- 如果在上一次数据接收结束之前 RxNE 位已置 1 但 DR 寄存器中的数据尚未读取,则 BTF 位会由硬件置 1,而接口会一直延长 SCL 低电平,等待 IIC_DR 寄存器被写入,以将 BTF 清零
- 通信结束:主设备会针对自从设备接收的最后一个字节发送 NACK。在接收到此 NACK 之后,从设备会释放对 SCL 和 SDA 线的控制。随后,主设备可发送一个停止位/重复起始位
- 为了在最后一个接收数据字节后生成非应答脉冲,必须在读取倒数第二个数据字节后(倒数第二个 RxNE 事件之后)立即将 ACK 位清零
- 要生成停止位/重复起始位,软件必须在读取倒数第二个数据字节后(倒数第二个 RxNE事件之后)将 STOP/START 位置 1
- 在只接收单个字节的情况下,会在 EV6 期间(在 ADDR 标志清零之前)禁止应答并在
- EV6 之后生成停止位。生成停止位后,接口会自动返回从模式(M/SL 位清零)
错误情况
- 总线错误:当 IIC 接口在传输地址或数据期间检测到外部停止位或起始位时,会出现此错误
- 应答失败:当接口检测到未应答脉冲会出现此错误
- 仲裁丢失:当 IIC 接口检测到仲裁丢失时会出现此错误
- 上溢/下溢错误:当时钟延长已禁止且 IIC 接口正在接收数据时,从模式中可能出现上溢错误。接口已经收到一个字节 (RxNE=1),但是收到下一个字节之前 DR 中的数据未被读走
配置
将 I2C 设置为 I2C,设置模式的话一般设置为主模式,参数设置如下
- I2C Speed Mode:I2C 速度模式
- Standard Mode:标准模式
- Fast Mode:快速模式
- I2C Clock Speed:I2C 时钟速度,这里设置为最大
- Clock No Stretch Mode:禁止时钟延长。clock stretching 通过将 SCL 线拉低来暂停一个传输,直到释放 SCL 线为高电平,传输才继续进行。clock stretching 是可选的,实际上大多数从设备不包括 SCL 驱动,所以它们不能 stretch 时钟
- Primary Address Length selection:从设备地址长度
- 7 bit
- 10 bit
- Dual Address Acknowledged:双地址确认
- Primary Slave Address:从设备初始地址
- General Call Address Detection:地址检测
1 | // I2C 主设备发送 |
DWT 软件定时器
在 Cortex-M 里面有一个外设叫 DWT(Data Watchpoint and Trace),是用于系统调试及跟踪,它有一个 32 位的寄存器叫 CYCCNT, 它是一个向上的计数器,记录的是内核时钟运行的个数,内核时钟跳动一次,该计数器就加 1,精度非常高,取决于内核的频率是多少
DWT 使用之前必须使能 DBG 的系统跟踪,DWT 的控制使能位在 DEMCR 寄存器的 bit24
DEMCR
DWT_CYCCNT 寄存器
使能 DWT_CYCCNT
寄存器之前,先清 0。其基地址是 0xE0001004,复位默认值是 0,可读写类型。所以往 0xE0001004 这个地址写就将 DWT_CYCCNT
清 0 了
DWT_CTRL 寄存器
配置
使能过程
- 先使能 DWT 外设,由内核调试寄存器
DEM_CR
的位 24 控制,写 1 使能 - 使能
CYCCNT
寄存器之前,先清 0 - 使能
CYCCNT
寄存器,由DWT_CTRL
的位 0 控制,写 1 使能
代码实现
头文件
1 |
|
源文件
1 |
|