ADC:
1.ADC 简介:
STM32F407ZGT6 有 3 个 ADC,每个 ADC 有 12 位、10 位、8 位和 6 位可选,每个 ADC 有 16 个外部通道。另外还有两个内部 ADC 源和 VBAT 通道挂在 ADC1 上。ADC 具有独立模式、双重模式和三重模式,对于不同 AD 转换要求几乎都有合适的模式可选。ADC 功能非常强大,具体的我们在功能框图中分析每个部分的功能。
2.ADC框图:

电压输入范围:
ADC 输入范围为:VREF- ≤ VIN ≤ VREF+。由 VREF-、VREF+ 、VDDA 、VSSA、这四个外部引脚决定。
我们在设计原理图的时候一般把 VSSA 和 VREF- 接地,把 VREF+ 和 VDDA 接 3V3,得到 ADC 的输入电压范围为:0~3.3V。如果我们想让输入的电压范围变宽,去到可以测试负电压或者更高的正电压,我们可以在外部加一个电压调理电路,把需要转换的电压抬升或者降压到 0~3.3V,这样 ADC 就可以测量了
输入通道:
我们确定好 ADC 输入电压之后,那么电压怎么输入到 ADC?
这里我们引入通道的概念,**STM32 的ADC 多达 19 个通道,其中外部的 16 个通道就是框图中的 ADCx_IN0、ADCx_IN1...ADCx_IN5。**这 16 个通道对应着不同的 IO 口,具体是哪一个 IO 口可以从手册查询到。
其中 ADC1/2/3 还有内部通道:ADC1 的通道 ADC1_IN16 连接到内部的 VSS,通道 ADC1_IN17 连接到了内部参考电压 VREFINT 连接,通道 ADC1_IN18 连接到了芯片内部的温度传感器或者备用电源 VBAT。ADC2和 ADC3 的通道 16、17、18 全部连接到了内部的 VSS。

外部的 16 个通道在转换的时候又分为规则通道和注入通道,其中规则通道最多有 16 路,注入通道最多有 4 路。那这两个通道有什么区别?在什么时候使用?
(1)规则通道:
规则通道:顾名思意,规则通道就是很规矩的意思,我们平时一般使用的就是这个通道,或者应该说我们用到的都是这个通道,没有什么特别要注意的可讲。
(2)注入通道:
注入,可以理解为插入,插队的意思,是一种不安分的通道。它是一种在规则通道转换的时候强行插入要转换的一种通道。如果在规则通道转换过程中,有注入通道插队,那么就要先转换完注入通道,等注入通道转换完成后,再回到规则通道的转换流程。这点跟中断程序很像,都是不安
分的主。所以,注入通道只有在规则通道存在时才会出现。
3.转换顺序:
规则序列:
规则序列规则序列寄存器有 3 个,分别为 SQR3、SQR2、SQR1:
SQR3 控制着规则序列中的第一个到第六个转换,对应的位为:SQ14:0~SQ64:0,第一次转换的是位 4:0 SQ14:0,如果通道 16 想第一次转换,那么在 SQ14:0 写 16 即可。SQR2 控制着规则序列中的第 7 到第 12 个转换,对应的位为:SQ74:0~SQ124:0,如果通道 1 想第 8 个转换,则 SQ84:0 写 1 即可。SQR1 控制着规则序列中的第 13 到第 16 个转换,对应位为:SQ134:0~SQ164:0,如果通道 6 想第 10 个转换,则SQ104:0 写 6 即可。具体使用多少个通道,由 SQR1 的位 L3:0 决定,最多 16 个通道。

注入序列:
**注入序列寄存器 JSQR 只有一个,最多支持 4 个通道,具体多少个由 JSQR 的 JL1:0 决定。**如果JL 的值小于 4 的话,则 JSQR 跟 SQR 决定转换顺序的设置不一样,第一次转换的不是 JSQR14:0,而是 JCQRx4:0 ,x =(4-JL),跟 SQR 刚好相反。如果 JL=00(1 个转换),那么转换的顺序是从 JSQR44:0 开始,而不是从 JSQR14:0 开始,这个要注意,编程的时候不要搞错。当 JL 等于4 时,跟 SQR 一样。

4.触发源:
通道选好了,转换的顺序也设置好了,那接下来就该开始转换了。
(1)ADC 转换可以由 ADC_CR2 的 ADON 这个位来控制,写 1 的时候开始转换,写 0 的时候停止转换,这个是最简单也是最好理解的开启 ADC 转换的控制方式,理解起来没啥技术含量。
(2)ADC 还支持外部事件触发转换,这个触发包括内部定时器触发和外部 IO 触发 。触发源有很多,具体选择哪一种触发源,由 ADC_CR2 的EXTSEL2:0 和 JEXTSEL2:0 位来控制。EXTSEL2:0 用于选择规则通道的触发源,JEXTSEL2:0用于选择注入通道的触发源。选定好触发源之后,触发源是否要激活,则由 ADC_CR2 的 EXTTRIG 和 JEXTTRIG 这两位来激活。如果使能了外部触发事件,我们还可以通过设置 ADC 控制寄存器 2:ADC_CR2 的 EXTEN1:0 和JEXTEN1:0 来控制触发极性,可以有 4 种状态,分别是:禁止触发检测、上升沿检测、下降沿检测以及上升沿和下降沿均检测。
5.转换时间:
ADC 时钟:
ADC 输入时钟 ADC_CLK 由 PCLK2 经过分频产生,最大值是 36MHz ,典型值为 30MHz,分频因子由 ADC 通用控制寄存器 ADC_CCR 的 ADCPRE1:0 设置,可设置的分频系数有 2、4、6 和8,注意这里没有 1 分频。对于 STM32F429IGT6 我们一般PCLK2=HCLK/2=84MHz。所以程序一般使用 4 分频或者 6 分频。(即21MHz/14MHz)
采样时间:
ADC 需要若干个 ADC_CLK 周期完成对输入的电压进行采样,采样的周期数可通过 ADC 采样时间寄存器 ADC_SMPR1 和 ADC_SMPR2 中的 SMP2:0 位设置,ADC_SMPR2 控制的是通道 0~9,ADC_SMPR1 控制的是通道 10~17。每个通道可以分别用不同的时间采样。其中采样周期最小是3 个,即如果我们要达到最快的采样,那么应该设置采样周期为 3 个周期,这里说的周期就是1/ADC_CLK。
ADC 的总转换时间跟 ADC 的输入时钟和采样时间有关,公式为:
Tconv = 采样时间 + 12 个周期(这个是选择12分辨率,在STM32F407还可以选择10、8、6分辨率)
当 ADCCLK = 30MHz,即 PCLK2 为 60MHz,ADC 时钟为 2 分频,采样时间设置为 3 个周期,那么总的转换时为:Tconv = 3 + 12 = 15 个周期 =0.5us。
一般我们设置 PCLK2=84MHz,经过 ADC 预分频器能分频到最大的时钟只能是 21M,采样周期设置为 3 个周期,算出最短的转换时间为 0.7142us,这个才是最常用的。
6.数据寄存器:
一切准备就绪后,ADC 转换后的数据根据转换组的不同,规则组的数据放在 ADC_DR 寄存器,注入组的数据放在 JDRx。如果是使用双重或者三重模式那规矩组的数据是存放在通用规矩寄存器 ADC_CDR 内的。
(1)规则数据寄存器 ADC_DR

ADC 规则组数据寄存器 ADC_DR 只有一个,是一个 32 位的寄存器,只有低 16 位有效并且只是用于独立模式存放转换完成数据 。因为 ADC 的最大精度是 12 位,ADC_DR 是 16 位有效,这样允许 ADC 存放数据时候选择左对齐或者右对齐,具体是以哪一种方式存放,由 ADC_CR2 的 11位 ALIGN 设置。假如设置 ADC 精度为 12 位,如果设置数据为左对齐,那 AD 转换完成数据存放在 ADC_DR 寄存器的 4:15 位内;如果为右对齐,则存放在 ADC_DR 寄存器的 0:11 位内。
规则通道可以有 16 个这么多,可规则数据寄存器只有一个,如果使用多通道转换,那转换的数据就全部都挤在了 DR 里面,前一个时间点转换的通道数据,就会被下一个时间点的另外一个通道转换的数据覆盖掉,所以当通道转换完成后就应该把数据取走,或者开启 DMA 模式,把数据传输到内存里面,不然就会造成数据的覆盖。最常用的做法就是开启 DMA 传输。如果没有使用 DMA 传输,我们一般都需要使用 ADC 状态寄存器 ADC_SR 获取当前 ADC 转换的进度状态,进而进行程序控制。

(2)注入数据寄存器 ADC_JDRx

ADC 注入组最多有 4 个通道,刚好注入数据寄存器也有 4 个,每个通道对应着自己的寄存器,不会跟规则寄存器那样产生数据覆盖的问题。
ADC_JDRx 是 32 位的,低 16 位有效,高 16 位保留,数据同样分为左对齐和右对齐,具体是以哪一种方式存放,由 ADC_CR2 的 11 位 ALIGN 设置。
(3)通用规则数据寄存器 ADC_CDR
规则数据寄存器 ADC_DR 是仅适用于独立模式的,而通用规则数据寄存器 ADC_CDR 是适用于双重和三重模式的。独立模式就是仅仅适用三个 ADC 的其中一个,双重模式就是同时使用 ADC1和 ADC2,而三重模式就是三个 ADC 同时使用。在双重或者三重模式下一般需要配合 DMA 数据传输使用。
7.三种模式
(1)首先是独立模式:
独立模式是最简单、最常用的工作模式。在这种模式下,三个 ADC(ADC1、ADC2、ADC3)各自独立工作,互不干扰。**每个 ADC 都使用自己的规则数据寄存器 ADC_DR 存放转换结果。**独立模式下,我们只需要配置单个 ADC 的通道、触发源、采样时间和数据对齐方式,然后启动转换即可。
独立模式适合只需要一路或几路模拟量采集的场景,比如采集一个电位器的电压、读取一个温度传感器的输出等。由于每个 ADC 独立运行,我们可以让 ADC1 采集电压、ADC2 采集电流、ADC3 采集温度,三者并行转换互不影响,提高了系统的采样效率。
在独立模式下,如果只使用单个通道,转换完成后直接从 ADC_DR 读取数据即可;如果使用多通道规则转换,由于只有一个 ADC_DR 寄存器,数据会被后一次转换覆盖,因此需要及时读取数据,或者开启 DMA 传输把数据搬运到内存中。
注意:每个ADC都有自己独立的规则组、注入组和数据寄存器,虽然手册没有像GPIOX那个加个X,但是它们的寄存器都是相互独立的,对于独立模式,假如规则组有多个通道要采样数据,那它只能先采样一个通道并转换数据才能对下一个通道实行同样操作
(2)双重模式(规则同时):


双重模式就是同时使用 ADC1 和 ADC2 两个 ADC 进行转换。在这种模式下,两个 ADC 可以同步采样,也可以交替采样,从而获得更高的采样率或者同时采集两路信号。
在双重模式下,规则组转换完成的数据不再存放在各自的 ADC_DR 中,而是统一存放在通用规则数据寄存器 ADC_CDR 中。ADC_CDR 是一个 32 位的寄存器,高 16 位存放 ADC2 的转换结果,低 16 位存放 ADC1 的转换结果,这样一次 DMA 传输就能把两个 ADC 的数据同时搬运到内存中,大大提高了数据传输效率。
由于双重模式涉及两个 ADC 的数据同步采集和搬运,数据量较大,一般都需要配合 DMA 传输使用,否则很容易因为来不及读取数据而造成数据覆盖丢失(虽然它们是同时的,但是也是分两次DMA传输)。
注意:虽然独立模式也能使用多个ADC采样数据,但是双重模式(规则同时)与独立模式不同的是:ADC1和ADC2是同时采样、同时转换数据、同时存放数据的,而独立模式做不到同时,每个ADC采样、转换、存储的时间都不相同
(3)三重模式(交替模式):
我们注意到在规则同组模式下同一通道不允许有多个ADC采样时间重叠,也就是说规则同组模式下不能多个ADC采样同一个通道,但交替模式可以实现,因为它避免了采样时间重叠。
交替模式是三重模式中非常有特色的一种工作方式:它让三个 ADC 轮流对同一个通道进行采样转换,每个 ADC 的采样时间在时间轴上错开,互不重叠,从而在不提高单个 ADC 采样率的前提下,等效地把采样率提高了三倍。
为什么规则同组模式不能多个 ADC 采样同一个通道?因为在规则同组模式下:三个 ADC 的采样保持电路同时挂到同一个引脚上,两个采样电容一起充放电会互相干扰,转换结果精度不保证。而交替模式通过把三个 ADC 的采样时刻在时间上错开,让 ADC1 先采样,紧接着 ADC2 采样,再接着 ADC3 采样,如此循环往复,每个 ADC 的采样时间都不重叠,这样就能对同一个通道实现三倍速率的连续采样。
在交替模式下,三个 ADC 转换完成的数据同样统一存放在通用规则数据寄存器 ADC_CDR 中。ADC_CDR 是一个 32 位的寄存器,在双重模式下高 16 位存放 ADC2 的结果、低 16 位存放 ADC1 的结果;而在三重模式下,由于有三个 ADC 的数据需要存放,一般需要配合 DMA 传输,把三个 ADC 的转换结果依次搬运到内存中,否则数据很容易因为来不及读取而被覆盖丢失。
三重模式(交替模式)特别适合需要高速采样 的场景,比如采集高频信号、电机电流波形分析等。通过三个 ADC 交替采样同一个通道,可以在不提高 ADC 时钟频率的前提下,获得更高的等效采样率,这是独立模式和双重模式都做不到的。


可以看到这里多了一个空闲时间,因为我们使用的是DMA模式二,在该模式下,当转换了两个数据才会发出一次DMA请求,如图一手册描述一样
8.代码示例
(1)独立模式中断:
(2)独立模式采样双通道数据,DMA搬运:
adc.c
c
#include "adc.h"
#define ADC_DRAddress (uint32_t)0x4001204C
volatile uint16_t ADC_Vlue[2];
void ADCIT_Init(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AN;
GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);
/*复位初始化DMA数据流*/
DMA_DeInit(DMA2_Stream0);
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_BufferSize = (uint32_t)2;
DMA_InitStruct.DMA_Channel = DMA_Channel_0;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;
/*M TO M必须开FIFO,软件就是不开,硬件也会开*/
DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStruct.DMA_PeripheralBurst =DMA_PeripheralBurst_Single ;
/*源地址*/
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)ADC_Vlue;
DMA_InitStruct.DMA_MemoryDataSize =DMA_MemoryDataSize_HalfWord ;
DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryInc =DMA_MemoryInc_Enable ;
DMA_InitStruct.DMA_PeripheralInc =DMA_PeripheralInc_Disable ;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
/*外设地址*/
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)ADC_DRAddress;
DMA_InitStruct.DMA_Priority =DMA_Priority_High ;
DMA_Init(DMA2_Stream0,&DMA_InitStruct);
/*清除数据流传输完成标志位*/
DMA_ClearFlag(DMA2_Stream0,DMA_FLAG_TCIF0);
DMA_Cmd(DMA2_Stream0,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
/*配置ADC_Common*/
ADC_CommonInitTypeDef ADC_CommonInitStruct;
/*选择DMA模式1*/
ADC_CommonInitStruct.ADC_DMAAccessMode=ADC_DMAAccessMode_1;
ADC_CommonInitStruct.ADC_Mode=ADC_Mode_Independent;
ADC_CommonInitStruct.ADC_Prescaler= ADC_Prescaler_Div4;
/*配置两个采样之间的延迟,独立模式用不到*/
ADC_CommonInitStruct.ADC_TwoSamplingDelay =ADC_TwoSamplingDelay_12Cycles ;
ADC_CommonInit(&ADC_CommonInitStruct);
ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;
ADC_InitStruct.ADC_DataAlign =ADC_DataAlign_Right ;
ADC_InitStruct.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
/*我们选择软件触发,边沿触发用不着*/
// ADC_InitStruct.ADC_ExternalTrigConv =ADC_ExternalTrigInjecConv_Ext_IT15;
ADC_InitStruct.ADC_NbrOfConversion = 2;
ADC_InitStruct.ADC_Resolution =ADC_Resolution_12b ;
/*扫描模式*/
ADC_InitStruct.ADC_ScanConvMode =ENABLE;
ADC_Init(ADC1,&ADC_InitStruct);
/*设置规则组优先级,采样周期*/
ADC_RegularChannelConfig(ADC1,ADC_Channel_8,1,ADC_SampleTime_56Cycles);
ADC_RegularChannelConfig(ADC1,ADC_Channel_9,2,ADC_SampleTime_56Cycles);
ADC_DMACmd(ADC1,ENABLE);
ADC_Cmd(ADC1,ENABLE);
/*使能软件触发ADC*/
ADC_SoftwareStartConv(ADC1);
}
ADC_DMACmd(ADC1,ENABLE);函数内部如下:


它访问的是CR2中的bit8
ADC_SoftwareStartConv(ADC1);而软件触发函数访问的也是CR2寄存器,可以看到置位是bit30,详细看手册:


| DMA 模式 1(你选的) | DMA 模式 2 | |
|---|---|---|
| CDR 的行为 | 当成两个 16 位槽位:同一地址连读两次,硬件轮流给两半(先 ADC1 低半字、再 ADC2 高半字) | 当成一个 32 位寄存器:一次整读拿一对 |
| DMA 配置 | 外设宽度 16 位,一次触发 → 搬运两次 | 外设宽度 32 位,一次触发 → 搬运一次 |
| 结果 | ADC_Vlue0=ADC1、ADC_Vlue1=ADC2 | 一个 uint32_t,高 16 ADC2、低 16 ADC1 |
这里我们选择的是 DMA 模式 1 ,所以 DMA 把 ADC_CDR 当成两个 16 位槽位来读取:同一地址连读两次,第一次读到 ADC1 的低半字,第二次读到 ADC2 的高半字。因此 DMA 外设宽度配置为 16 位(DMA_PeripheralDataSize_HalfWord),一次 DMA 触发会搬运两次数据,分别存入 ADC_Vlue[0] 和 ADC_Vlue[1]。
如果选择 DMA 模式 2 ,则 DMA 把 ADC_CDR 当成一个 32 位寄存器,一次整读就能拿到 ADC1 和 ADC2 的一对数据,此时外设宽度应配置为 32 位(DMA_PeripheralDataSize_Word),内存中用一个 uint32_t 变量接收,高 16 位是 ADC2、低 16 位是 ADC1。
main.c:
c
extern volatile uint16_t ADC_Vlue[2];
float ADC_ChangeVlue[2];
int main(void)
{
Systick_Init();
USARTConfig_Init();
ADCIT_Init();
SPI_Error(1);
while(1)
{
ADC_ChangeVlue[0]=(float)((uint16_t)ADC_Vlue[0]*(3.3/4096));
ADC_ChangeVlue[1]=(float)((uint16_t)ADC_Vlue[1]*(3.3/4096));
Delay_ms(1000);
printf("\r\nADC1采样:%f\n",ADC_ChangeVlue[0]);
printf("\r\nADC2采样:%f\n",ADC_ChangeVlue[1]);
Delay_ms(1000);
}
}
(3)双重模式(规则同时模式):
adc.c
c
#include "adc.h"
#define ADC_DRAddress (uint32_t)0x4001204C
#define ADC_CDRAddress (uint32_t)0x40012308
volatile uint16_t ADC_Vlue[2];
void ADCIT_Init(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AN;
GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);
/*复位初始化DMA数据流*/
DMA_DeInit(DMA2_Stream0);
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_BufferSize = (uint32_t)2;
DMA_InitStruct.DMA_Channel = DMA_Channel_0;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;
/*M TO M必须开FIFO,软件就是不开,硬件也会开*/
DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStruct.DMA_PeripheralBurst =DMA_PeripheralBurst_Single ;
/*源地址*/
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)ADC_Vlue;
DMA_InitStruct.DMA_MemoryDataSize =DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryInc =DMA_MemoryInc_Enable ;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
/*外设地址*/
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)ADC_CDRAddress;
DMA_InitStruct.DMA_PeripheralInc =DMA_PeripheralInc_Disable ;
DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_Priority =DMA_Priority_High ;
DMA_Init(DMA2_Stream0,&DMA_InitStruct);
/*清除数据流传输完成标志位*/
DMA_ClearFlag(DMA2_Stream0,DMA_FLAG_TCIF0);
DMA_Cmd(DMA2_Stream0,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC2,ENABLE);
/*配置ADC_Common*/
ADC_CommonInitTypeDef ADC_CommonInitStruct;
/*选择DMA模式1*/
ADC_CommonInitStruct.ADC_DMAAccessMode=ADC_DMAAccessMode_1;
/*双ADC规则模式*/
ADC_CommonInitStruct.ADC_Mode=ADC_DualMode_RegSimult;
ADC_CommonInitStruct.ADC_Prescaler= ADC_Prescaler_Div4;
/*配置两个采样之间的延迟,独立模式用不到*/
ADC_CommonInitStruct.ADC_TwoSamplingDelay =ADC_TwoSamplingDelay_12Cycles ;
ADC_CommonInit(&ADC_CommonInitStruct);
ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;
ADC_InitStruct.ADC_DataAlign =ADC_DataAlign_Right ;
/*我们选择软件触发,边沿触发用不着*/
ADC_InitStruct.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
// ADC_InitStruct.ADC_ExternalTrigConv =ADC_ExternalTrigInjecConv_Ext_IT15;
ADC_InitStruct.ADC_NbrOfConversion = 1;
ADC_InitStruct.ADC_Resolution =ADC_Resolution_12b ;
/*扫描模式*/
ADC_InitStruct.ADC_ScanConvMode =DISABLE;
ADC_Init(ADC1,&ADC_InitStruct);
ADC_Init(ADC2,&ADC_InitStruct);
/*设置规则组优先级,采样周期*/
ADC_RegularChannelConfig(ADC1,ADC_Channel_8,1,ADC_SampleTime_3Cycles);
ADC_RegularChannelConfig(ADC2,ADC_Channel_9,1,ADC_SampleTime_3Cycles);
ADC_MultiModeDMARequestAfterLastTransferCmd(ENABLE);
ADC_Cmd(ADC1,ENABLE);
ADC_Cmd(ADC2,ENABLE);
/*使能软件触发ADC*/
ADC_SoftwareStartConv(ADC1);
// ADC_SoftwareStartConv(ADC2);
}
可以看到与独立模式不同,这里的数据寄存器是CDR寄存器,详细如下:

而且它使能DMA的函数也不一样了,我们进到该函数内部看看

可以看到它访问的并不是CR2寄存器,而是CCR的DDS位,详细如下:

从上图我们了解到,这个bit位作用于多个ADC模式,在描述中(DMA=01、10和11)是指DMA模式1、2、3,而DMA模式在我们配置ADC通用结构体时已经配置好了,我们去这个结构体内部看看,它访问的是哪个寄存器吧

可以看到,它访问的也是CCR寄存器,再看该变量的宏定义

具体详细如下:


在双重模式下,虽然我们只调用了 ADC_SoftwareStartConv(ADC1) 触发 ADC1 开始转换,但由于 ADC1 和 ADC2 配置的是规则同时模式 ,两者是同步采样、同步转换的。ADC1 转换完成后会向 DMA 发出请求,此时 DMA 的源地址指向的是通用规则数据寄存器 ADC_CDR,而不是 ADC1 自己的 ADC_DR。
那为什么 DMA 搬运的是 CDR 里的数据,而 CDR 里又为什么会有 ADC2 的数据呢?这是因为在双重模式下,ADC1 和 ADC2 转换完成的数据会先分别存入各自的 ADC_DR 寄存器 ,然后硬件会自动把 ADC1_DR 和 ADC2_DR 的数据合并搬运到 ADC_CDR 中------高 16 位放 ADC2 的结果,低 16 位放 ADC1 的结果。然后触发一次DMA,DMA 就会从 ADC_CDR 分两次把两个 ADC 的数据搬运到内存(DMA模式1)。
所以整个数据流向是:ADC1_DR / ADC2_DR → ADC_CDR → 内存 。这也是为什么双重模式下 DMA 的外设地址要配置成 ADC_CDRAddress(0x40012308),而不是独立模式下的 ADC_DRAddress(0x4001204C)。
(4)三重模式(交替模式):
c
#include "adc.h"
#define ADC_DRAddress (uint32_t)0x4001204C
#define ADC_CDRAddress (uint32_t)0x40012308
volatile uint32_t ADC_Vlue[3];
void ADCIT_Init(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AN;
GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_2;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOC,&GPIO_InitStruct);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);
/*复位初始化DMA数据流*/
DMA_DeInit(DMA2_Stream0);
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_BufferSize = 3;
DMA_InitStruct.DMA_Channel = DMA_Channel_0;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;
/*M TO M必须开FIFO,软件就是不开,硬件也会开*/
DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStruct.DMA_PeripheralBurst =DMA_PeripheralBurst_Single ;
/*源地址*/
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)ADC_Vlue;
DMA_InitStruct.DMA_MemoryDataSize =DMA_MemoryDataSize_Word;
DMA_InitStruct.DMA_MemoryInc =DMA_MemoryInc_Enable ;
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
/*外设地址*/
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)ADC_CDRAddress;
DMA_InitStruct.DMA_PeripheralInc =DMA_PeripheralInc_Disable ;
DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Word;
DMA_InitStruct.DMA_Priority =DMA_Priority_High ;
DMA_Init(DMA2_Stream0,&DMA_InitStruct);
/*清除数据流传输完成标志位*/
DMA_ClearFlag(DMA2_Stream0,DMA_FLAG_TCIF0);
DMA_Cmd(DMA2_Stream0,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC3,ENABLE);
/*配置ADC_Common*/
ADC_CommonInitTypeDef ADC_CommonInitStruct;
/*选择DMA模式1*/
ADC_CommonInitStruct.ADC_DMAAccessMode=ADC_DMAAccessMode_2;
/*三重交替模式*/
ADC_CommonInitStruct.ADC_Mode=ADC_TripleMode_Interl;
ADC_CommonInitStruct.ADC_Prescaler= ADC_Prescaler_Div4;
/*配置两个采样之间的延迟,独立模式用不到*/
ADC_CommonInitStruct.ADC_TwoSamplingDelay =ADC_TwoSamplingDelay_8Cycles ;
ADC_CommonInit(&ADC_CommonInitStruct);
ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;
ADC_InitStruct.ADC_DataAlign =ADC_DataAlign_Right ;
/*我们选择软件触发,边沿触发用不着*/
ADC_InitStruct.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
// ADC_InitStruct.ADC_ExternalTrigConv =ADC_ExternalTrigInjecConv_Ext_IT15;
ADC_InitStruct.ADC_NbrOfConversion = 1;
ADC_InitStruct.ADC_Resolution =ADC_Resolution_12b ;
/*扫描模式*/
ADC_InitStruct.ADC_ScanConvMode =DISABLE;
ADC_Init(ADC1,&ADC_InitStruct);
ADC_Init(ADC2,&ADC_InitStruct);
ADC_Init(ADC3,&ADC_InitStruct);
/*设置规则组优先级,采样周期*/
ADC_RegularChannelConfig(ADC1,ADC_Channel_12,1,ADC_SampleTime_3Cycles);
ADC_RegularChannelConfig(ADC2,ADC_Channel_12,1,ADC_SampleTime_3Cycles);
ADC_RegularChannelConfig(ADC3,ADC_Channel_12,1,ADC_SampleTime_3Cycles);
ADC_MultiModeDMARequestAfterLastTransferCmd(ENABLE);
ADC_Cmd(ADC1,ENABLE);
ADC_Cmd(ADC2,ENABLE);
ADC_Cmd(ADC3,ENABLE);
/*使能软件触发ADC*/
ADC_SoftwareStartConv(ADC1);
// ADC_SoftwareStartConv(ADC2);
}
注意这里不能用ADC1、2、3共同采集PB0,因为对于ADC3,它的8号通道跟ADC1、2上是不一样,我们去看引脚表

可以看到对于ADC1、2,PB0就是通道8,但是对于ADC3呢,它的通道8是PF10,所以我们要用PC2引脚,气死我了,调试半天,忘记这茬
main.c
c
extern volatile uint32_t ADC_Vlue[3];
float ADC_ChangeVlue[3];
int main(void)
{
Systick_Init();
USARTConfig_Init();
ADCIT_Init();
SPI_Error(1);
while(1)
{
ADC_ChangeVlue[0]=(float)((uint16_t)ADC_Vlue[0]*(3.3/4096));
ADC_ChangeVlue[1]=(float)((uint16_t)ADC_Vlue[1]*(3.3/4096));
ADC_ChangeVlue[2]=(float)((uint16_t)ADC_Vlue[2]*(3.3/4096));
printf("\r\nADC1采样:0x%08x\n",ADC_Vlue[0]);
printf("\r\nADC1采样:0x%08x\n",ADC_Vlue[1]);
printf("\r\nADC1采样:0x%08x\n",ADC_Vlue[2]);
printf("\r\nADC1采样:%f\n",ADC_ChangeVlue[0]);
printf("\r\nADC2采样:%f\n",ADC_ChangeVlue[1]);
printf("\r\nADC3采样:%f\n",ADC_ChangeVlue[2]);
Delay_ms(1000);
}
}
CAN:
1.CAN 协议简介:
CAN 是控制器局域网络 (Controller Area Network) 的简称,它是由研发和生产汽车电子产品著称的德国 BOSCH 公司开发的,并最终成为国际标准(ISO11519),是国际上应用最广泛的现场总线之一。
CAN 总线协议已经成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线,并且拥有以CAN 为底层协议专为大型货车和重工机械车辆设计的 J1939 协议。近年来,它具有的高可靠性和良好的错误检测能力受到重视,被广泛应用于汽车计算机控制系统和环境温度恶劣、电磁辐射
强及振动大的工业环境。
2.CAN 物理层:
与 I2C、SPI 等具有时钟信号的同步通讯方式不同,CAN 通讯并不是以时钟信号来进行同步的,它是一种异步通讯,只具有 CAN_High 和 CAN_Low 两条信号线,共同构成一组差分信号线,以差分信号的形式进行通讯。
(1)闭环总线网络
CAN 物理层的形式主要有两种,图 CAN 闭环总线通讯网络 中的 CAN 通讯网络是一种遵循ISO11898 标准的高速、短距离"闭环网络",它的总线最大长度为 40m,通信速度最高为 1Mbps,总线的两端各要求有一个"120 欧"的电阻。

(2)开环总线网络
图 CAN 开环总线通讯网络 中的是遵循 ISO11519-2 标准的低速、远距离"开环网络",它的最大传输距离为 1km,最高通讯速率为 125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个"2.2 千欧"的电阻。

(3)通讯节点
从 CAN 通讯网络图可了解到,CAN 总线上可以挂载多个通讯节点,节点之间的信号经过总线传输,实现节点间通讯。由于 CAN 通讯协议不对节点进行地址编码,而是对数据内容进行编码的,所以网络中的节点个数理论上不受限制,只要总线的负载足够即可,可以通过中继器增强负载。
那"对数据内容进行编码"是什么意思呢?这里我们用一个车载的例子来理解:假设总线上挂载了刹车、窗户、电脑等多个节点,它们与 I2C 通讯不同,每个节点都能主动发送和接收信息 。假如电脑通过总线向所有负载发送"关窗户"的信息,此时窗户和刹车负载都能接收到这条信息,接着它们会通过这段信息前面的一段 ID 号 来辨别这条信息是否是发送给自己的------ID 号匹配的节点(窗户)才会执行相应动作,ID 号不匹配的节点(刹车)则忽略这条信息。这就是 CAN 协议"对数据内容编码"的含义:通过数据帧中的 ID 标识来区分不同的消息,而不是像 I2C 那样通过节点地址来寻址。
CAN 通讯节点由一个 CAN 控制器及 CAN 收发器组成,控制器与收发器之间通过 CAN_Tx 及CAN_Rx 信号线相连,收发器与 CAN 总线之间使用 CAN_High 及 CAN_Low 信号线相连。其中CAN_Tx 及 CAN_Rx 使用普通的类似 TTL 逻辑信号,而 CAN_High 及 CAN_Low 是一对差分信号线,使用比较特别的差分信号,下一小节再详细说明。
当 CAN 节点需要发送数据时,控制器把要发送的二进制编码通过 CAN_Tx 线发送到收发器,然后由收发器把这个普通的逻辑电平信号转化成差分信号,通过差分线 CAN_High 和 CAN_Low 线输出到 CAN 总线网络。而通过收发器接收总线上的数据到控制器时,则是相反的过程,收发器
把总线上收到的 CAN_High 及 CAN_Low 信号转化成普通的逻辑电平信号,通过 CAN_Rx 输出到控制器中。
(4)差分信号
差分信号又称差模信号,与传统使用单根信号线电压表示逻辑的方式有区别,使用差分信号传输时,需要两根信号线,这两个信号线的振幅相等,相位相反,通过两根信号线的电压差值来表示逻辑 0 和逻辑 1。见图差分信号 ,它使用了 V+ 与 V-信号的差值表达出了图下方的信号。

相对于单信号线传输的方式,使用差分信号传输具有如下优点:
• 抗干扰能力强,当外界存在噪声干扰时,几乎会同时耦合到两条信号线上,而接收端只关心两个信号的差值,所以外界的共模噪声可以被完全抵消。(例如当外界噪声干扰让V+、V-都增加或减少了5V,但由于它们的逻辑的电平是通过这两根线差值来表示的,所以这个噪声对这个电平没有任何影响)
• 能有效抑制它对外部的电磁干扰,同样的道理,由于两根信号的极性相反,他们对外辐射的电磁场可以相互抵消,耦合的越紧密,泄放到外界的电磁能量越少。
• 时序定位精确,由于差分信号的开关变化是位于两个信号的交点,而不像普通单端信号依靠高低两个阈值电压判断,因而受工艺,温度的影响小,能降低时序上的误差,同时也更适合于低幅度信号的电路。
由于差分信号线具有这些优点,所以在 USB 协议、485 协议、以太网协议及 CAN 协议的物理层中,都使用了差分信号传输。
(5)CAN 协议中的差分信号
CAN 协议中对它使用的 CAN_High 及 CAN_Low 表示的差分信号做了规定,见表 CAN 协议标准表示的信号逻辑 及图 CAN 的差分信号高速。
**以高速 CAN 协议为例,当表示逻辑 1 时 (隐性电平),CAN_High 和 CAN_Low 线上的电压均为 2.5v,即它们的电压差 VH-VL=0V;而表示逻辑 0 时 (显性电平),CAN_High 的电平为 3.5V,CAN_Low 线的电平为 1.5V,即它们的电压差为 VH-VL=2V。**例如,当 CAN 收发器从 CAN_Tx 线接收到来自 CAN 控制器的低电平信号时 (逻辑 0),它会使CAN_High 输出 3.5V,同时 CAN_Low 输出 1.5V,从而输出显性电平表示逻辑 0。

在 CAN 总线中,必须使它处于隐性电平 (逻辑 1) 或显性电平 (逻辑 0) 中的其中一个状态。假如有两个 CAN 通讯节点,在同一时间,一个输出隐性电平,另一个输出显性电平,类似 I2C 总线的"线与"特性将使它处于显性电平状态,显性电平的名字就是这样来的,即可以认为显性具有优先的意味。
由于 CAN 总线协议的物理层只有 1 对差分线,在一个时刻只能表示一个信号,所以对通讯节点来说,CAN 通讯是半双工的,收发数据需要分时进行。在 CAN 的通讯网络中,因为共用总线,在整个网络中同一时刻只能有一个通讯节点发送信号,其余的节点在该时刻都只能接收。
3.协议层
以上是 CAN 的物理层标准,约定了电气特性,以下介绍的协议层则规定了通讯逻辑。
(1)CAN 的波特率及位同步
由于 CAN 属于异步通讯,没有时钟信号线,连接在同一个总线网络中的各个节点会像串口异步通讯那样,节点间使用约定好的波特率进行通讯,特别地:CAN 还会使用"位同步"的方式来抗干扰、吸收误差,实现对总线电平信号进行正确的采样,确保通讯正常。
位时序分解:
为了实现位同步,**CAN 协议把每一个数据位的时序分解成如图 CAN 位时序分解图 所示的 SS 段、PTS 段、PBS1 段、PBS2 段,这四段的长度加起来即为一个 CAN 数据位的长度。分解后最小的时间单位是 Tq,而一个完整的位由 8~25 个 Tq 组成。**为方便表示,图 CAN 位时序分解图 中的高低电平直接代表信号逻辑 0 或逻辑 1(不是差分信号)。

该图中表示的 CAN 通讯信号每一个数据位的长度为 19Tq,其中 SS 段占 1Tq,PTS 段占 6Tq,PBS1段占 5Tq,PBS2 段占 7Tq。信号的采样点位于 PBS1 段与 PBS2 段之间(大概在这个数据位的百分之70),通过控制各段的长度,可以对采样点的位置进行偏移,以便准确地采样。(也就是说这个PBS1和PBS2是能改变长度的)
各段的作用如介绍下:
• SS 段 (SYNC SEG)
SS 译为同步段,若通讯节点检测到总线上信号的跳变沿被包含在 SS 段的范围之内,则表示节点与总线的时序是同步的,当节点与总线同步时,采样点采集到的总线电平即可被确定为该位的电平。SS 段的大小固定为 1Tq。
• PTS 段 (PROP SEG)
PTS 译为传播时间段,这个时间段是用于补偿网络的物理延时时间。是总线上输入比较器延时和输出驱动器延时总和的两倍。PTS 段的大小可以为 1~8Tq。
• PBS1 段 (PHASE SEG1),
PBS1 译为相位缓冲段,主要用来补偿边沿阶段的误差,它的时间长度在重新同步的时候可以加长。PBS1 段的初始大小可以为 1~8Tq。
• PBS2 段 (PHASE SEG2)
PBS2 这是另一个相位缓冲段,也是用来补偿边沿阶段误差的,它的时间长度在重新同步时可以缩短。PBS2 段的初始大小可以为 2~8Tq。
(2)通讯的波特率
总线上的各个通讯节点只要约定好 1 个 Tq 的时间长度以及每一个数据位占据多少个 Tq,就可以确定 CAN 通讯的波特率。
例如,假设上图中的 1Tq=1us,而每个数据位由 19 个 Tq 组成,则传输一位数据需要时间 T1bit=19us,从而每秒可以传输的数据位个数为:
1* 10的6次方 /19 = 52631.6 (bps)
这个每秒可传输的数据位的个数即为通讯中的波特率。
(3)同步过程分析
波特率只是约定了每个数据位的长度,数据同步还涉及到相位的细节,这个时候就需要用到数据位内的 SS、PTS、PBS1 及 PBS2 段了。
根据对段的应用方式差异,CAN 的数据同步分为硬同步和重新同步。其中硬同步只是当存在"帧起始信号"时起作用,无法确保后续一连串的位时序都是同步的,而重新同步方式可解决该问题,这两种方式具体介绍如下:
(3.1)硬同步
若某个 CAN 节点通过总线发送数据时,它会发送一个表示通讯起始的信号 (即下一小节介绍的帧起始信号),该信号是一个由高变低的下降沿。而挂载到 CAN 总线上的通讯节点在不发送数据时,会时刻检测总线上的信号。
见图硬同步过程图 ,可以看到当总线出现帧起始信号时,某节点检测到总线的帧起始信号不在节点内部时序的 SS 段范围 ,所以判断它自己的内部时序与总线不同步,因而这个状态的采样点采集得的数据是不正确的。所以节点以硬同步的方式调整,把自己的位时序中的 SS 段平移至总
线出现下降沿的部分,获得同步,同步后采样点就可以采集得正确数据了。

(3.2) 重新同步
前面的硬同步只是当存在帧起始信号时才起作用,如果在一帧很长的数据内,节点信号与总线信号相位有偏移时,这种同步方式就无能为力了。因而需要引入重新同步方式,它利用普通数据位的高至低电平的跳变沿来同步 (帧起始信号是特殊的跳变沿)。重新同步与硬同步方式相似的地方是它们都使用 SS 段来进行检测,同步的目的都是使节点内的 SS 段把跳变沿包含起来。
重新同步的方式分为超前和滞后两种情况 ,以总线跳变沿与 SS 段的相对位置进行区分。第一种相位超前的情况如图相位超前时的重新同步 ,节点从总线的边沿跳变中,检测到它内部的时序比总线的时序相对超前 2Tq,这时控制器在下一个位时序中的 PBS1 段增加 2Tq 的时间长度,使得节点与总线时序重新同步。

第二种相位滞后的情况如图相位滞后时的重新同步 ,节点从总线的边沿跳变中,检测到它的时序比总线的时序相对滞后 2Tq,这时控制器在前一个位时序中的 PBS2 段减少 2Tq 的时间长度,获得同步。

**在重新同步的时候,PBS1 和 PBS2 中增加或减少的这段时间长度被定义为"重新同步补偿宽度SJW (reSynchronization Jump Width)"。**一般来说 CAN 控制器会限定 SJW 的最大值,如限定了最大 SJW=3Tq 时,单次同步调整的时候不能增加或减少超过 3Tq 的时间长度,若有需要,控制器会通过多次小幅度调整来实现同步。当控制器设置的 SJW 极限值较大时,可以吸收的误差加大,但通讯的速度会下降。
这里有些没看明白的同学可能疑惑为什么会出现节点内部时序与总线起始帧不一样?因为有同学会误以为位时序是对数据位的拆分
其实位时序不是"对数据位做拆分",而是节点内部一台永远在走的时钟
CAN 没有单独的时钟线(异步串行总线),所以每个节点内部都有一个晶振驱动的计数器。这个计数器不管总线上有没有数据,一直在跑------它把时间预先切成连续的小格子,每个格子 = 一个标称位周期= 若干 Tq(SS+PBS1+PBS2 只是格子内部的再划分)。
数据位不是被"拆"出来的,而是**"装进"格子的**:接收方在每个格子的固定位置(采样点 = BS1 末尾)看一眼总线电平,这一眼看到什么,这一位就是什么。
打个比方:你手腕上的表,秒针不管你看不看都在走。节点内部的位时序计数器就是那块表------它预先把时间切好格子,数据来了往里装。
那么两台节点为什么对不齐?
- 频率差:每个节点晶振独立。标称 8MHz 的两块晶振,实际可能是 8.0001 和 7.9998。A 的一个"位周期"和 B 的天然差一点点。
- 相位随机:A 开始发帧的那一刻,B 的计数器正转到自己格子的哪个位置------完全随机,可能在 SS 段,也可能在格子正中间。
所以帧起始下降沿落进 B 的格子时,大概率不在 SS 段里。B 怎么知道没对齐?就看这个沿的落点。
(3.3)回到两种同步:
硬同步:SOF 下降沿(空闲=高,起始=低)一出现,B 直接把自己的计数器归零重打------把格子边界搬过去,让沿落在新格子的 SS 段里。相当于广播报时"滴"的一声,你把秒针拨回 0。每帧一次。
重新同步:长帧里没有第二次报时,但两块表的频率差会让格子慢慢错位(几十上百个位累积下来就歪了)。好在数据位里还会出现"高→低"跳变沿(比如 1 后面跟 0),B
拿它微调:发现自己跑快了(沿落得比预想靠前)→ 把下一个格子的 PBS1 拉长几个 Tq;跑慢了 → 缩短 PBS2。一次最多调 SJW 个 Tq,防止调过头。
顺带一个细节:重新同步只认"隐性→显性"(高→低)的沿------显性电平是节点主动把总线拉低的,边沿干净利落;释放回隐性时各节点松手有先后,边沿是糊的,不能当基准。
4.CAN 的报文种类及结构
在 SPI 通讯中,片选、时钟信号、数据输入及数据输出这 4 个信号都有单独的信号线,I2C 协议包含有时钟信号及数据信号 2 条信号线,异步串口包含接收与发送 2 条信号线,这些协议包含的信号都比 CAN 协议要丰富,它们能轻易进行数据同步或区分数据传输方向。
而 CAN 使用的是两条差分信号线,只能表达一个信号,简洁的物理层决定了 CAN 必然要配上一套更复杂的协议,如何用一个信号通道实现同样、甚至更强大的功能呢?CAN 协议给出的解决方案是对数据、操作命令 (如读/写) 以及同步信号进行打包,打包后的这些内容称为报文。
(1)报文的种类
在原始数据段的前面加上传输起始标签、片选 (识别) 标签和控制标签,在数据的尾段加上 CRC校验标签、应答标签和传输结束标签,把这些内容按特定的格式打包好,就可以用一个通道表达各种信号了,各种各样的标签就如同 SPI 中各种通道上的信号,起到了协同传输的作用。当整个数据包被传输到其它设备时,只要这些设备按格式去解读,就能还原出原始数据,这样的报文就被称为 CAN 的"数据帧"。
为了更有效地控制通讯,CAN 一共规定了 5 种类型的帧,它们的类型及用途说明如表帧的种类及其用途。

(2)数据帧的结构
数据帧是在 CAN 通讯中最主要、最复杂的报文,我们来了解它的结构,见图数据帧的结构。

数据帧以一个显性位 (逻辑 0) 开始,以 7 个连续的隐性位 (逻辑 1) 结束,在它们之间,分别有仲裁段、控制段、数据段、CRC 段和 ACK 段。
• 帧起始
SOF 段 (Start Of Frame),译为帧起始,帧起始信号只有一个数据位,是一个显性电平,它用于通知各个节点将有数据传输,其它节点通过帧起始信号的电平跳变沿来进行硬同步。
• 仲裁段
当同时有两个报文被发送时,总线会根据仲裁段的内容决定哪个数据包能被传输,这也是它名称的由来。
仲裁段的内容主要为本数据帧的 ID 信息 (标识符),数据帧具有标准格式和扩展格式两种 ,区别就在于 ID 信息的长度,标准格式的 ID 为 11 位,扩展格式的 ID 为 29 位,它在标准 ID 的基础上多出 18 位。在 CAN 协议中,ID 起着重要的作用,它决定着数据帧发送的优先级,也决定着其它节点是否会接收这个数据帧。CAN 协议不对挂载在它之上的节点分配优先级和地址,对总线的占有权是由信息的重要性决定的,即对于重要的信息,我们会给它打包上一个优先级高的 ID,使它能够及时地发送出去。也正因为它这样的优先级分配原则,使得 CAN 的扩展性大大加强,在总线上增加或减少节点并不影响其它设备。
报文的优先级,是通过对 ID 的仲裁来确定的。根据前面对物理层的分析我们知道如果总线上同时出现显性电平和隐性电平,总线的状态会被置为显性电平,CAN 正是利用这个特性进行仲裁。
若两个节点同时竞争 CAN 总线的占有权,当它们发送报文时,若首先出现隐性电平,则会失去对总线的占有权,进入接收状态。
见图仲裁过程 ,在开始阶段,两个设备发送的电平一样,所以它们一直继续发送数据。到了图中箭头所指的时序处,节点单元 1 发送的为隐性电平,而此时节点单元 2 发送的为显性电平,由于总线的"线与"特性使它表达出显示电平,因此单元 2 竞争总线成功,这个报文得以被继续发送出去。

仲裁段 ID 的优先级也影响着接收设备对报文的反应。**因为在 CAN 总线上数据是以广播的形式发送的,所有连接在 CAN 总线的节点都会收到所有其它节点发出的有效数据,因而我们的 CAN控制器大多具有根据 ID 过滤报文的功能,它可以控制自己只接收某些 ID 的报文。**回看图数据帧的结构 中的数据帧格式,可看到仲裁段除了报文 ID 外,还有 RTR、IDE 和 SRR 位。
(1) RTR 位 (Remote Transmission Request Bit),译作远程传输请求位,它是用于区分数据帧和遥控帧的,当它为显性电平时表示数据帧,隐性电平时表示遥控帧。
(2) IDE 位 (Identifier Extension Bit),译作标识符扩展位,它是用于区分标准格式与扩展格式,当它为显性电平时表示标准格式,隐性电平时表示扩展格式。
(3) SRR 位 (Substitute Remote Request Bit),只存在于扩展格式,它用于替代标准格式中的 RTR 位。由于扩展帧中的 SRR 位为隐性位,RTR 在数据帧为显性位,所以在两个 ID 相同的标准格式报文与扩展格式报文中,标准格式的优先级较高。
• 控制段
在控制段中的 r1 和 r0 为保留位,默认设置为显性位。它最主要的是 DLC 段 (Data Length Code),译为数据长度码,它由 4 个数据位组成,用于表示本报文中的数据段含有多少个字节,DLC 段表示的数字为 0~8。
• 数据段
数据段为数据帧的核心内容,它是节点要发送的原始信息,由 0~8 个字节组成,MSB 先行。
• CRC 段
为了保证报文的正确传输,CAN 的报文包含了一段 15 位的 CRC 校验码,一旦接收节点算出的CRC 码跟接收到的 CRC 码不同,则它会向发送节点反馈出错信息,利用错误帧请求它重新发送。CRC 部分的计算一般由 CAN 控制器硬件完成,出错时的处理则由软件控制最大重发数。在 CRC 校验码之后,有一个 CRC 界定符,它为隐性位,主要作用是把 CRC 校验码与后面的 ACK段间隔起来。
• ACK 段
ACK 段包括一个 ACK 槽位,和 ACK 界定符位。类似 I2C 总线,在 ACK 槽位中,发送节点发送的是隐性位,而接收节点则在这一位中发送显性位以示应答。在 ACK 槽和帧结束之间由 ACK 界定符间隔开。
• 帧结束
EOF 段 (End Of Frame),译为帧结束,帧结束段由发送节点发送的 7 个隐性位表示结束。
(3)其它报文的结构
关于其它的 CAN 报文结构,不再展开讲解,其主要内容见图各种 CAN 报文的结构。

5.STM32 的 CAN 外设简介
STM32 的芯片中具有 bxCAN 控制器 (Basic Extended CAN),它支持 CAN 协议 2.0A 和 2.0B 标准。该 CAN 控制器支持最高的通讯速率为 1Mb/s;可以自动地接收和发送 CAN 报文,支持使用标准ID 和扩展 ID 的报文;外设中具有 3 个发送邮箱,发送报文的优先级可以使用软件控制,还可以记录发送的时间;具有 2 个 3 级深度的接收 FIFO,可使用过滤功能只接收或不接收某些 ID 号的报文;可配置成自动重发;不支持使用 DMA 进行数据收发。

STM32 的有两组 CAN 控制器,其中 CAN1 是主设备,框图中的"存储访问控制器"是由 CAN1控制的,CAN2 无法直接访问存储区域,所以使用 CAN2 的时候必须使能 CAN1 外设的时钟。框图中主要包含 CAN 控制内核、发送邮箱、接收 FIFO 以及验收筛选器,下面对框图中的各个部分进行介绍。
(1)1.CAN 控制内核
框图中标号处的 CAN 控制内核包含了各种控制寄存器及状态寄存器,我们主要讲解其中的主控制寄存器 CAN_MCR 及位时序寄存器 CAN_BTR。主控制寄存器 CAN_MCR主控制寄存器 CAN_MCR 负责管理 CAN 的工作模式,它使用以下寄存器位实现控制。
(1) DBF 调试冻结功能
DBF(Debug freeze) 调试冻结,使用它可设置 CAN 处于工作状态或禁止收发的状态,禁止收发时仍可访问接收 FIFO 中的数据。这两种状态是当 STM32 芯片处于程序调试模式时才使用的,平时使用并不影响。
(2) TTCM 时间触发模式
TTCM(Time triggered communication mode) 时间触发模式,它用于配置 CAN 的时间触发通信模式,在此模式下,CAN 使用它内部定时器产生时间戳,并把它保存在CAN_RDTxR、CAN_TDTxR 寄存器中。内部定时器在每个 CAN 位时间累加,在接收和发送的帧起始位被采样,并生成时间戳。利用它可以实现 ISO 11898-4CAN 标准的分时同步通信功能。
(3) ABOM 自动离线管理
ABOM(Automatic bus-off management) 自动离线管理,它用于设置是否使用自动离线管理功能。当节点检测到它发送错误或接收错误超过一定值时,会自动进入离线状态,在离线状态中,CAN 不能接收或发送报文。处于离线状态的时候,可以软件控制恢复或者直接使用这个自动离线管理功能,它会在适当的时候自动恢复。
(4) AWUM 自动唤醒
AWUM(Automatic bus-off management),自动唤醒功能,CAN 外设可以使用软件进入低功耗的睡眠模式,如果使能了这个自动唤醒功能,当 CAN 检测到总线活动的时候,会自动唤醒。
(5) NART 自动重传
NART(No automatic retransmission) 报文自动重传功能,设置这个功能后,当报文发送
(6) RFLM 锁定模式
RFLM(Receive FIFO locked mode)FIFO 锁定模式,该功能用于锁定接收 FIFO。锁定后,当接收 FIFO 溢出时,会丢弃下一个接收的报文。若不锁定,则下一个接收到的报文会覆盖原报文。
(7) TXFP 报文发送优先级的判定方法
TXFP(Transmit FIFO priority) 报文发送优先级的判定方法,当 CAN 外设的发送邮箱中有多个待发送报文时,本功能可以控制它是根据报文的 ID 优先级还是报文存进邮箱的顺序来发送。
(2)位时序寄存器 (CAN_BTR) 及波特率
CAN 外设中的位时序寄存器 CAN_BTR 用于配置测试模式、波特率以及各种位内的段参数。
(1) 测试模式
为方便调试,STM32 的 CAN 提供了测试模式,配置位时序寄存器 CAN_BTR 的 SILM 及 LBKM寄存器位可以控制使用正常模式、静默模式、回环模式及静默回环模式 ,见图四种工作模式。

各个工作模式介绍如下:
• 正常模式
正常模式下就是一个正常的 CAN 节点,可以向总线发送数据和接收数据。
• 静默模式
静默模式下,它自己的输出端的逻辑 0 数据会直接传输到它自己的输入端,逻辑 1 可以被发送到总线,所以它不能向总线发送显性位 (逻辑 0),只能发送隐性位 (逻辑 1)。输入端可以从总线接收内容。由于它只可发送的隐性位不会强制影响总线的状态,所以把它称为静默模式。这种模式一般用于监测,它可以用于分析总线上的流量,但又不会因为发送显性位而影响总线。
• 回环模式
回环模式下,它自己的输出端的所有内容都直接传输到自己的输入端,输出端的内容同时也会被传输到总线上,即也可使用总线监测它的发送内容。输入端只接收自己发送端的内容,不接收来自总线上的内容。使用回环模式可以进行自检。
• 回环静默模式
回环静默模式是以上两种模式的结合,自己的输出端的所有内容都直接传输到自己的输入端,并且不会向总线发送显性位影响总线,不能通过总线监测它的发送内容。输入端只接收自己发送端的内容,不接收来自总线上的内容。这种方式可以在"热自检"时使用,即自我检查的时候,不会干扰总线。
以上说的各个模式,是不需要修改硬件接线的,例如:当输出直连输入时,它是在 STM32 芯片内部连接的,传输路径不经过 STM32 的 CAN_Tx/Rx 引脚,更不经过外部连接的 CAN 收发器,只有输出数据到总线或从总线接收的情况下才会经过 CAN_Tx/Rx 引脚和收发器。
(2) 位时序及波特率
STM32 外设定义的位时序与我们前面解释的 CAN 标准时序有一点区别,见图 STM32 中 CAN 的位时序。

STM32 的 CAN 外设位时序中只包含 3 段,分别是同步段 SYNC_SEG、位段 BS1 及位段 BS2,采样点位于 BS1 及 BS2 段的交界处。其中 SYNC_SEG 段固定长度为 1Tq,而 BS1 及 BS2 段可以在位时序寄存器 CAN_BTR 设置它们的时间长度,它们可以在重新同步期间增长或缩短,该长度SJW 也可在位时序寄存器中配置。
理解 STM32 的 CAN 外设的位时序时,可以把它的 BS1 段理解为是由前面介绍的 CAN 标准协议中 PTS 段与 PBS1 段合在一起的,而 BS2 段就相当于 PBS2 段。
了解位时序后,我们就可以配置波特率了。通过配置位时序寄存器 CAN_BTR 的 TS13:0 及TS22:0 寄存器位设定 BS1 及 BS2 段的长度后,我们就可以确定每个 CAN 数据位的时间:
BS1 段时间:
TS1=Tq x (TS13:0 + 1)
BS2 段时间:
TS2= Tq x (TS22:0 + 1),
一个数据位的时间:
T1bit =1Tq+TS1+TS2 =1+ (TS13:0 + 1)+ (TS22:0 + 1)= N Tq
其中单个时间片的长度 Tq 与 CAN 外设的所挂载的时钟总线及分频器配置有关,CAN1 和 CAN2外设都是挂载在 APB1 总线上的,而位时序寄存器 CAN_BTR 中的 BRP9:0 寄存器位可以设置CAN 外设时钟的分频值,所以:
Tq = (BRP9:0+1) x TPCLK
其中的 PCLK 指 APB1 时钟,默认值为 42MHz。
最终可以计算出 CAN 通讯的波特率:
BaudRate = 1/N Tq
例如表一种配置波特率为 1Mbps 的方式 说明了一种把波特率配置为 1Mbps 的方式。 
(3)CAN 发送邮箱:
回到 CAN 外设框图,在标号处的是 CAN 外设的发送邮箱,它一共有 3 个发送邮箱,即最多可以缓存 3 个待发送的报文。

每个发送邮箱中包含有标识符寄存器 CAN_TIxR、数据长度控制寄存器 CAN_TDTxR 及 2 个数据寄存器 CAN_TDLxR、CAN_TDHxR ,它们的功能如下图

当我们要使用 CAN 外设发送报文时,把报文的各个段分解,按位置写入到这些寄存器中,并对标识符寄存器 CAN_TIxR 中的发送请求寄存器位 TMIDxR_TXRQ 置 1,即可把数据发送出去。
其中标识符寄存器 CAN_TIxR 中的 STDID 寄存器位比较特别。我们知道 CAN 的标准标识符的总位数为 11 位,而扩展标识符的总位数为 29 位的。当报文使用扩展标识符的时候,标识符寄存器 CAN_TIxR 中的 STDID10:0 等效于 EXTID18:28 位,它与 EXTID17:0 共同组成完整的 29位扩展标识符。
(4)CAN 接收 FIFO
CAN 外设框图,在标号处的是 CAN 外设的接收 FIFO,它一共有 2 个接收 FIFO,每个 FIFO 中有 3 个邮箱,即最多可以缓存 6 个接收到的报文。
当接收到报文时,FIFO 的报文计数器会自增,而 STM32 内部读取 FIFO 数据之后,报文计数器会自减,我们通过状态寄存器可获知报文计数器的值,而通过前面主控制寄存器的 RFLM 位,可设置锁定模式,锁定模式下 FIFO 溢出时会丢弃新报文,非锁定模式下 FIFO 溢出时新报文会覆盖旧报文。跟发送邮箱类似,每个接收 FIFO 中包含有标识符寄存器 CAN_RIxR、数据长度控制寄存器CAN_RDTxR 及 2 个数据寄存器 CAN_RDLxR、CAN_RDHxR,它们的功能见表发送邮箱的寄存器。

通过中断或状态寄存器知道接收 FIFO 有数据后,我们再读取这些寄存器的值即可把接收到的报文加载到 STM32 的内存中。
(5)验收筛选器
CAN 外设框图,在标号处的是 CAN 外设的验收筛选器,一共有 28 个筛选器组,每个筛选器组有 2 个寄存器,CAN1 和 CAN2 共用的筛选器的。(0到13个筛选组是CAN1的,14到27是CAN2的)
在 CAN 协议中,消息的标识符与节点地址无关,但与消息内容有关。因此,发送节点将报文广播给所有接收器时,接收节点会根据报文标识符的值来确定软件是否需要该消息,为了简化软件的工作,STM32 的 CAN 外设接收报文前会先使用验收筛选器检查,只接收需要的报文到 FIFO
中。
筛选器工作的时候,可以调整筛选 ID 的长度及过滤模式。根据筛选 ID 长度来分类有有以下两种:
(1) 检查 STDID10:0、EXTID17:0、IDE 和 RTR 位,一共 31 位。
(2) 检查 STDID10:0、RTR、IDE 和 EXTID17:15,一共 16 位。
通过配置筛选尺度寄存器 CAN_FS1R 的 FSCx 位可以设置筛选器工作在哪个尺度。
而根据过滤的方法分为以下两种模式:
(1) 标识符列表模式,它把要接收报文的 ID 列成一个表,要求报文 ID 与列表中的某一个标识符完全相同才可以接收,可以理解为白名单管理。
(2) 掩码模式,它把可接收报文 ID 的某几位作为列表,这几位被称为掩码,可以把它理解成关键字搜索,只要掩码 (关键字) 相同,就符合要求,报文就会被保存到接收 FIFO。
通过配置筛选模式寄存器 CAN_FM1R 的 FBMx 位可以设置筛选器工作在哪个模式。
不同的尺度和不同的过滤方法可使筛选器工作在图筛选器的 4 种工作状态 的 4 种状态。

每组筛选器包含 2 个 32 位的寄存器,分别为 CAN_FxR1 和 CAN_FxR2,它们用来存储要筛选的ID 或掩码,各个寄存器位代表的意义与图中两个寄存器下面"映射"的一栏一致,各个模式的说明见表筛选器的工作状态说明。

例如:下面的表格所示,在掩码模式时,第一个寄存器存储要筛选的 ID,第二个寄存器存储掩码,掩码为 1 的部分表示该位必须与 ID 中的内容一致,筛选的结果为表中第三行的 ID 值,它是一组包含多个的 ID 值,其中 x 表示该位可以为 1 可以为 0。这就可以筛选出更多的ID

而工作在标识符模式时,2 个寄存器存储的都是要筛选的 ID,它只包含 2 个要筛选的 ID 值 (32位模式时)。
如果使能了筛选器,且报文的 ID 与所有筛选器的配置都不匹配,CAN 外设会丢弃该报文,不存入接收 FIFO。
(6)整体控制逻辑
回到CAN 外设架构图,图中的标号处表示的是 CAN2 外设的结构,它与CAN1 外设是一样的,他们共用筛选器且由于存储访问控制器由 CAN1 控制,所以要使用 CAN2的时候必须要使能 CAN1 的时钟。

(7)CAN 初始化结构体
(1)CAN 初始化结构体:
c
typedef struct {
uint16_t CAN_Prescaler; /* 配置 CAN 外设的时钟分频,可设置为 1-1024*/
uint8_t CAN_Mode; /* 配置 CAN 的工作模式,回环或正常模式 */
uint8_t CAN_SJW; /* 配置 SJW 极限值 */
uint8_t CAN_BS1; /* 配置 BS1 段长度 */
uint8_t CAN_BS2; /* 配置 BS2 段长度 */
FunctionalState CAN_TTCM; /* 是否使能 TTCM 时间触发功能 */
FunctionalState CAN_ABOM; /* 是否使能 ABOM 自动离线管理功能 */
FunctionalState CAN_AWUM; /* 是否使能 AWUM 自动唤醒功能 */
FunctionalState CAN_NART; /* 是否使能 NART 自动重传功能 */
FunctionalState CAN_RFLM; /* 是否使能 RFLM 锁定 FIFO 功能 */
FunctionalState CAN_TXFP; /* 配置 TXFP 报文优先级的判定方法 */
} CAN_InitTypeDef;
(2)CAN 发送及接收结构体
c
* @brief CAN Tx message structure definition
* 发送结构体
*/
typedef struct {
uint32_t StdId; /* 存储报文的标准标识符 11 位,0-0x7FF. */
uint32_t ExtId; /* 存储报文的扩展标识符 29 位,0-0x1FFFFFFF. */
uint8_t IDE; /* 存储 IDE 扩展标志 */
uint8_t RTR; /* 存储 RTR 远程帧标志 */
uint8_t DLC; /* 存储报文数据段的长度,0-8 */
uint8_t Data[8]; /* 存储报文数据段的内容 */
} CanTxMsg;
/**
* @brief CAN Rx message structure definition
* 接收结构体
*/
typedef struct {
uint32_t StdId; /* 存储了报文的标准标识符 11 位,0-0x7FF. */
uint32_t ExtId; /* 存储了报文的扩展标识符 29 位,0-0x1FFFFFFF. */
uint8_t IDE; /* 存储了 IDE 扩展标志 */
uint8_t RTR; /* 存储了 RTR 远程帧标志 */
uint8_t DLC; /* 存储了报文数据段的长度,0-8 */
uint8_t Data[8]; /* 存储了报文数据段的内容 */
uint8_t FMI; /* 存储了本报文是由经过哪个筛选器进的FIFO,0-0xFF */
} CanRxMsg;
(3)CAN 筛选器结构体
c
/**
* @brief CAN filter init structure definition
* CAN 筛选器结构体
*/
typedef struct {
uint16_t CAN_FilterIdHigh; /*CAN_FxR1 寄存器的高 16 位 */
uint16_t CAN_FilterIdLow; /*CAN_FxR1 寄存器的低 16 位 */
uint16_t CAN_FilterMaskIdHigh; /*CAN_FxR2 寄存器的高 16 位 */
uint16_t CAN_FilterMaskIdLow; /*CAN_FxR2 寄存器的低 16 位 */
uint16_t CAN_FilterFIFOAssignment; /* 设置经过筛选后数据存储到哪个接收 FIFO
uint8_t CAN_FilterNumber; /* 筛选器编号,范围 0-27*/
uint8_t CAN_FilterMode; /* 筛选器模式 */
uint8_t CAN_FilterScale; /* 设置筛选器的尺度 */
FunctionalState CAN_FilterActivation; /* 是否使能本筛选器 */
} CAN_FilterInitTypeDef;

(8)实验代码:
can.c
c
#include "can.h"
void Can_Init(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
/*开启GPIO复用功能*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF;
GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_12|GPIO_Pin_13;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
/*选择哪个复用功能,这里选择SPI1*/
GPIO_PinAFConfig(GPIOB,GPIO_PinSource13,GPIO_AF_CAN2);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource12,GPIO_AF_CAN2);
/*使用CAN2,要开启CAN1时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN2,ENABLE);
/*初始化CAN2*/
CAN_InitTypeDef CAN_InitStruct;
CAN_InitStruct.CAN_BS1 =CAN_BS1_4tq;
CAN_InitStruct.CAN_BS2 = CAN_BS2_2tq;
CAN_InitStruct.CAN_Mode = CAN_Mode_LoopBack;
CAN_InitStruct.CAN_Prescaler =6;
CAN_InitStruct.CAN_SJW = CAN_SJW_2tq;
CAN_InitStruct.CAN_TTCM = DISABLE; //使能的话,加入时间戳,记录什么时候发送接受数据
CAN_InitStruct.CAN_ABOM = ENABLE;
CAN_InitStruct.CAN_AWUM = ENABLE;
CAN_InitStruct.CAN_NART = ENABLE;
CAN_InitStruct.CAN_RFLM = DISABLE; //使能的话,当FIFO装满数据,会丢弃新的数据
CAN_InitStruct.CAN_TXFP = DISABLE; //使能的话,优先级会按时间顺序排列
CAN_Init(CAN2,&CAN_InitStruct);
/*初始化筛选器*/
CAN_FilterInitTypeDef CAN_FilterInitStruct;
CAN_FilterInitStruct.CAN_FilterActivation =ENABLE;
CAN_FilterInitStruct.CAN_FilterFIFOAssignment =CAN_Filter_FIFO0; //设置经过筛选后数据存储到哪个接收 FIFO
CAN_FilterInitStruct.CAN_FilterIdHigh =0x0000; //配置筛选ID,符合该ID可以通过筛选进来
CAN_FilterInitStruct.CAN_FilterIdLow =0x0000;
CAN_FilterInitStruct.CAN_FilterMaskIdHigh =0x0000; //本节遮掩码都配置0,所以我们收到的ID都可以进来
CAN_FilterInitStruct.CAN_FilterMaskIdLow =0x0000;
CAN_FilterInitStruct.CAN_FilterMode =CAN_FilterMode_IdMask; //选择掩码模式
CAN_FilterInitStruct.CAN_FilterNumber =14; //筛选器编号,CAN1是0到13,CAN2是14到27
CAN_FilterInitStruct.CAN_FilterScale =CAN_FilterScale_32bit;
CAN_FilterInit(&CAN_FilterInitStruct);
/*使能FIFO接受中断*/
CAN_ITConfig(CAN2,CAN_IT_FMP0,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel=CAN2_RX0_IRQn; //我们选择的FIFO0
NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE ;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =1 ;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStruct);
}
stm32f4xx_it.c
c
extern uint8_t Rx_Flag;
extern CanRxMsg Rx_Msg;
void CAN2_RX0_IRQHandler(void)
{
if(CAN_GetITStatus(CAN2,CAN_IT_FMP0)==1)
{
/*读取数据,清除标志位*/
CAN_Receive(CAN2,CAN_FIFO0,&Rx_Msg);
Rx_Flag=1;
}
}
main.c
c
#include "spi_flash.h"
#include "can.h"
CanTxMsg Tx_Msg;
CanRxMsg Rx_Msg;
uint8_t Rx_Flag=0;
uint8_t mailbox;
int main(void)
{
uint8_t i=0;
Systick_Init();
LED_Init();
USARTConfig_Init();
Can_Init();
Key_Init();
printf("\r\n实验开始\n");
/*初始化发送报文结构体*/
Tx_Msg.StdId = 0;
Tx_Msg.ExtId = 0x1314;
Tx_Msg.IDE = CAN_Id_Extended; //选择标志帧还是扩展帧
Tx_Msg.DLC =8 ;
Tx_Msg.RTR = CAN_RTR_Data;
for(i=0;i<Tx_Msg.DLC;i++)
{
Tx_Msg.Data[i]=i;
}
/*初始化接受报文结构体*/
Rx_Msg.StdId =0;
Rx_Msg.ExtId =0x1314;
Rx_Msg.IDE = CAN_Id_Extended;
Rx_Msg.DLC =8;
Rx_Msg.RTR = CAN_RTR_Data;
printf("\r\n准备发送\n");
while(1)
{
/*如果按下按键,就会发送数据*/
if(Key_State()==1)
{
/*返回存储你发送报文的邮箱号*/
mailbox=CAN_Transmit(CAN2,&Tx_Msg);
/*判断是否有空的邮箱*/
if(mailbox!=CAN_TxStatus_NoMailBox)
{
/*等待报文发送到邮箱*/
while(CAN_TransmitStatus(CAN2,mailbox)!=CAN_TxStatus_Ok);
/*等到CAN收发器将报文发送到总线上*/
Delay_ms(100);
printf("\r\n发送成功\n");
}
else
{
printf("\r\n无空邮箱\n");
}
}
if(Rx_Flag==1)
{
CAN_Receive(CAN2,CAN_FIFO0,&Rx_Msg);
printf("\r\nRx_Msg.StdId:%x\n",Rx_Msg.StdId);
printf("\r\nRx_Msg.ExtId:%x\n",Rx_Msg.ExtId);
printf("\r\nRx_Msg.IDE:%x\n",Rx_Msg.IDE);
printf("\r\nRx_Msg.DLC:%x\n",Rx_Msg.DLC);
printf("\r\nRx_Msg.RTR:%x\n",Rx_Msg.RTR);
for(i=0;i<Rx_Msg.DLC;i++)
{
printf("\r\nRxMeg_Data[%d]:0x%x\n",i,Rx_Msg.Data[i]);
}
Rx_Flag=0;
}
}
}
如果我们的掩码不设置成0,而且F,那么我们就要根据下面的图来配置标识符

根据上面的图,我们知道如果我们选择32位的筛选器,那么低三位就要分别配置IDE、RTR、0,代码如下:
c
#include "can.h"
void Can_Init(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
/*开启GPIO复用功能*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF;
GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_12|GPIO_Pin_13;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
/*选择哪个复用功能,这里选择SPI1*/
GPIO_PinAFConfig(GPIOB,GPIO_PinSource13,GPIO_AF_CAN2);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource12,GPIO_AF_CAN2);
/*使用CAN2,要开启CAN1时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN2,ENABLE);
/*初始化CAN2*/
CAN_InitTypeDef CAN_InitStruct;
CAN_InitStruct.CAN_BS1 =CAN_BS1_4tq;
CAN_InitStruct.CAN_BS2 = CAN_BS2_2tq;
CAN_InitStruct.CAN_Mode = CAN_Mode_LoopBack;
CAN_InitStruct.CAN_Prescaler =6;
CAN_InitStruct.CAN_SJW = CAN_SJW_2tq;
CAN_InitStruct.CAN_TTCM = DISABLE; //使能的话,加入时间戳,记录什么时候发送接受数据
CAN_InitStruct.CAN_ABOM = ENABLE;
CAN_InitStruct.CAN_AWUM = ENABLE;
CAN_InitStruct.CAN_NART = ENABLE;
CAN_InitStruct.CAN_RFLM = DISABLE; //使能的话,当FIFO装满数据,会丢弃新的数据
CAN_InitStruct.CAN_TXFP = DISABLE; //使能的话,优先级会按时间顺序排列
CAN_Init(CAN2,&CAN_InitStruct);
/*初始化筛选器*/
CAN_FilterInitTypeDef CAN_FilterInitStruct;
CAN_FilterInitStruct.CAN_FilterActivation =ENABLE;
CAN_FilterInitStruct.CAN_FilterFIFOAssignment =CAN_Filter_FIFO0; //设置经过筛选后数据存储到哪个接收 FIFO
/*配置筛选ID,符合该ID可以通过筛选进来,这里右移16位是为了得到高十六位*/
CAN_FilterInitStruct.CAN_FilterIdHigh =(((uint32_t)0x1314<<3|CAN_Id_Extended|CAN_RTR_Data)&0xFFFF0000)>>16;
CAN_FilterInitStruct.CAN_FilterIdLow =(((uint32_t)0x1314<<3|CAN_Id_Extended|CAN_RTR_Data)&0x0000FFFF);
CAN_FilterInitStruct.CAN_FilterMaskIdHigh =0xFFFF; //本节遮掩码都配置0,所以我们收到的ID都可以进来
CAN_FilterInitStruct.CAN_FilterMaskIdLow =0xFFFF;
CAN_FilterInitStruct.CAN_FilterMode =CAN_FilterMode_IdMask; //选择掩码模式
CAN_FilterInitStruct.CAN_FilterNumber =14; //筛选器编号,CAN1是0到13,CAN2是14到27
CAN_FilterInitStruct.CAN_FilterScale =CAN_FilterScale_32bit;
CAN_FilterInit(&CAN_FilterInitStruct);
/*使能FIFO接受中断*/
CAN_ITConfig(CAN2,CAN_IT_FMP0,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel=CAN2_RX0_IRQn; //我们选择的FIFO0
NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE ;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =1 ;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStruct);
}
定时器:
1.定时器类型图:

2.基本定时器:
(1)框图:

基本定时器的功能框图包含了基本定时器最核心内容,掌握了功能框图,对基本定时器就有一个整体的把握,在编程时思路就非常清晰,见图基本定时器功能框图 。
首先先看图基本定时器功能框图 中绿色框内容,第一个是带有阴影的方框,方框内容一般是一个寄存器名称,比如图中主体部分的自动重载寄存器 (TIMx_ARR) 或 PSC 预分频器 (TIMx_PSC),这里要特别突出的是阴影这个标志的作用,它表示这个寄存器还自带有影子寄存器,在硬件结构上实际是有两个寄存器,源寄存器是我们可以进行读写操作,而影子寄存器我们是完全无法操作的,有内部硬件使用。影子寄存器是在程序运行时真正起到作用的,源寄存器只是给我们读写用的,只有在特定时候 (特定事件发生时) 才把源寄存器的值拷贝给它的影子寄存器。多个影子寄存器一起使用可以到达同步更新多个寄存器内容的目的。
**接下来是一个指向右下角的图标,它表示一个事件,而一个指向右上角的图标表示中断和 DMA输出。**这个我们把它放在图中主体更好理解。图中的自动重载寄存器有影子寄存器,它左边有一个带有"U"字母的事件图标,表示在更新事件生成时就把自动重载寄存器内容拷贝到影子寄存器内,这个与上面分析是一致。寄存器右边的事件图标、中断和 DMA 输出图标表示在自动重载寄存器值与计数器寄存器值相等时生成事件、中断和 DMA 输出。
(2)时钟源
定时器要实现计数必须有个时钟源,基本定时器时钟只能来自内部时钟,高级控制定时器和通用定时器还可以选择外部时钟源或者直接来自其他定时器等待模式**。我们可以通过 RCC 专用时钟配置寄存器 (RCC_DCKCFGR) 的 TIMPRE 位设置所有定时器的时钟频率,我们一般设置该位为默认值 0,即 TIMxCLK 为总线时钟的两倍,使得表各个定时器特性 中可选的最大定时器时钟为84MHz,即基本定时器的内部时钟 (CK_INT) 频率为 84MHz。**

基本定时器只能使用内部时钟,当 TIM6 和 TIM7 控制寄存器 1(TIMx_CR1) 的 CEN 位置 1 时,启动基本定时器,并且预分频器的时钟来源就是 CK_INT。对于高级控制定时器和通用定时器的时钟源可以来找控制器外部时钟、其他定时器等等模式,较为复杂,我们在相关章节会详细介绍。
(3)控制器
定时器控制器控制实现定时器功能,控制定时器复位、使能、计数是其基础功能,基本定时器还专门用于 DAC 转换触发。
(4)计数器
基本定时器计数过程主要涉及到三个寄存器内容,分别是计数器寄存器 (TIMx_CNT)、预分频器寄存器 (TIMx_PSC)、自动重载寄存器 (TIMx_ARR),这三个寄存器都是 16 位有效数字,即可设置值为 0 至 65535。
首先我们来看图基本定时器功能框图 中预分频器 PSC,它有一个输入时钟 CK_PSC 和一个输出时钟 CK_CNT。输入时钟 CK_PSC 来源于控制器部分,基本定时器只有内部时钟源所以 CK_PSC 实际等于 CK_INT,即 84MHz(基本定时器)。在不同应用场所,经常需要不同的定时频率,通过设置预分频器 PSC的值可以非常方便得到不同的 CK_CNT,实际计算为:fCK_CNT 等于 fCK_PSC/(PSC15:0+1)。
图基本定时器时钟源分频 是将预分频器 PSC 的值从 1 改为 4 时计数器时钟变化过程。原来是 1 分频,CK_PSC 和 CK_CNT 频率相同。向 TIMx_PSC 寄存器写入新值时,并不会马上更新 CK_CNT输出频率,而是等到更新事件发生时,把 TIMx_PSC 寄存器值更新到影子寄存器中,使其真正产生效果。更新为 4 分频后,在 CK_PSC 连续出现 4 个脉冲后 CK_CNT 才产生一个脉冲 。

在定时器使能 (CEN 置 1) 时,计数器 COUNTER 根据 CK_CNT 频率向上计数,即每来一个CK_CNT 脉冲,TIMx_CNT 值就加 1。当 TIMx_CNT 值与 TIMx_ARR 的设定值相等时就自动生成事件并 TIMx_CNT 自动清零,然后自动重新开始计数,如此重复以上过程。为此可见,我们只要设置 CK_PSC 和 TIMx_ARR 这两个寄存器的值就可以控制事件生成的时间,而我们一般的应用程序就是在事件生成的回调函数中运行的。在 TIMx_CNT 递增至与 TIMx_ARR 值相等,我们叫做为定时器上溢。
自动重载寄存器 TIMx_ARR 用来存放于计数器值比较的数值,如果两个数值相等就生成事件,将相关事件标志位置位,生成 DMA 和中断输出。
TIMx_ARR 有影子寄存器,可以通过 TIMx_CR1寄存器的 ARPE 位控制影子寄存器功能,如果 ARPE 位置 1,影子寄存器有效,只有在事件更新时才把 TIMx_ARR 值赋给影子寄存器。如果 ARPE 位为 0,修改 TIMx_ARR 值马上有效。
(5)定时器周期计算:
公式:需要设置的时间=时钟源/(PSC-1)(预分频器)/(ARR-1)(自动重装载),举例:假如我要设置1ms的定时器,时钟源频率是84MHZ,那么我就可以设置PSC=839,ARR=99
(6)定时器初始化结构体详解
基本定时器-1 定时器基本初始化结构:
c
typedef struct {
uint16_t TIM_Prescaler; // 预分频器
uint16_t TIM_CounterMode; // 计数模式
uint32_t TIM_Period; // 定时器周期
uint16_t TIM_ClockDivision; // 时钟分频
uint8_t TIM_RepetitionCounter; // 重复计算器
} TIM_TimeBaseInitTypeDef;
(1) TIM_Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器时钟,它设定 TIMx_PSC寄存器的值。可设置范围为 0 至 65535,实现 1 至 65536 分频。
(2) TIM_CounterMode:定时器计数方式,可是在为向上计数、向下计数以及三种中心对齐模式。基本定时器只能是向上计数,即 TIMx_CNT 只能从 0 开始递增,并且无需初始化。
(3) TIM_Period:定时器周期,实际就是设定自动重载寄存器的值,在事件生成时更新到影子寄存器。可设置范围为 0 至 65535。
(4) TIM_ClockDivision:时钟分频,设置定时器时钟 CK_INT 频率与数字滤波器采样时钟频率分频比,基本定时器没有此功能,不用设置。
(5) TIM_RepetitionCounter:重复计数器(当计数器的值与ARR值相等,重复计数器的值就会减一次,你可以设置重复计数器的值),属于高级控制寄存器专用寄存器位,利用它可以非常容易控制输出 PWM 的个数。这里不用设置。
(7)实验代码:
tim.c
c
#include "tim.h"
void BaseTim_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_Period =100-1;
TIM_TimeBaseInitStruct.TIM_Prescaler =840-1;
TIM_TimeBaseInit(TIM6,&TIM_TimeBaseInitStruct);
/*清除上溢标志位*/
TIM_ClearFlag(TIM6,TIM_IT_Update);
/*中断申请*/
TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE);
TIM_Cmd(TIM6,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel=TIM6_DAC_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE ;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =1 ;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStruct);
}
stm32f4xx_it.c
c
extern uint16_t Tim_Flag;
void TIM6_DAC_IRQHandler(void)
{
if(TIM_GetITStatus(TIM6,TIM_IT_Update)!=RESET)
{
Tim_Flag++;
TIM_ClearITPendingBit(TIM6,TIM_IT_Update);
}
}
main.c
c
uint16_t Tim_Flag=0;
int main(void)
{
uint8_t i=0;
Systick_Init();
LED_Init();
USARTConfig_Init();
BaseTim_Init();
while(1)
{
if(Tim_Flag>=1000)
{
LED1_TOGGLE;
Tim_Flag=0;
}
}
}
高级定时器:
(1)定时器从模式:
定时器引脚分部图:

高级定时器框图:


(1.1)时钟源:
高级控制定时器有四个时钟源可选:
• 内部时钟源 CK_INT
• 外部时钟模式 1:外部输入引脚 TIx(x=1,2)或ETR
• 外部时钟模式 2:外部触发输入 ETR
• 内部触发输入(ITR0~3)
(1.2)滤波器:
如果来自外部的时钟信号的频率过高或者混杂有高频干扰信号的话,我们就需要使用滤波器对ETRP 信号重新采样,来达到降频或者去除高频干扰的目的,具体的由 TIMx_CCMx 的位 ICxF3:0配置。
(1.3)边沿检测:
边沿检测的信号来自于滤波器的输出,在成为触发信号之前,需要进行边沿检测,决定是上升沿有效还是下降沿有效,具体的由 TIMx_CCER 的位 CCxP 和 CCxNP 配置。
(1.4)触发选择:
当使用外部时钟模式 1 时,触发源有两个,一个是滤波后的定时器输入 1(TI1FP1)和滤波后的定时器输入 2(TI2FP2),具体的由 TIMxSMCR 的位 TS2:0 配置。
(1.5)内部触发输入:
内部触发输入是使用一个定时器作为另一个定时器的预分频器。硬件上高级控制定时器和通用定时器在内部连接在一起,可以实现定时器同步或级联。主模式的定时器可以对从模式定时器执行复位、启动、停止或提供时钟。高级控制定时器和部分通用定时器 (TIM2 至 TIM5) 可以设置
为主模式或从模式,TIM9 和 TIM10 可设置为从模式。
(1.6)内部时钟源 (CK_INT)
内部时钟 CK_INT 即来自于芯片内部,等于 168M,一般情况下,我们都是使用内部时钟。当从模式控制寄存器 TIMx_SMCR 的 SMS 位等于 000 时,则使用内部时钟。
(1.7)我们先来分析序号1:
我们可以看到在TRGI连接着从模式控制器和左边的触发器,而触发器是选择哪个信号作为输入信号作为从模式的信号源,那么连接触发器的信号有哪些呢?我们可以看到分别是ETR、ITR0~3(内部触发输入)、TI1FP1、TI2FP2,它们的区别是什么?下面我来给那么介绍一下。
首先我们要了解从模式控制器:

我们可以看到从模式控制可以选择如上图中的几种模式,其中外部时钟模式 1 就是我们经常用到的。该模式下,我们可以选择外部输入引脚(TIx)或 ETR 作为计数器时钟,从而对外部引脚的脉冲信号进行计数。那么什么是外部时钟模式 2,ETR 又有什么作用呢?下面我们来详细分析。
(1.8)外部时钟模式 2 与 ETR 的作用
ETR(External Trigger,外部触发输入)是定时器的一个外部引脚,它既可以作为外部时钟源通道,也可以作为触发输入。外部时钟模式 2 就是专门利用 ETR 引脚作为计数器时钟源的一种模式,它只需要在 TIMx_SMCR 寄存器中置位 ECE 位即可启用,不占用从模式控制器。
情况一:ETR 作为外部时钟源通道
看名字我们就知道 ETR 是作为外部时钟源通道。当我们要测试一个模块的电平信号时,就可以让ETR 选择外部时钟模式 2,这样我们就可以通过计数器来计算外部模块的电平变化次数
情况二:ETR 与从模式控制器组合的进阶玩法
假设从模式被配置成复位模式,由于从模式控制器不再是外部时钟模式 1(而是复位模式),也就不能为定时器提供计数信号,所以我们需要使用另外的时钟源。如果单纯的计时,可以使用内部时钟;想对外部信号进行计数,也可以使用外部触发器 ETR,通过外部时钟模式 2 接入定时器。这就是为何 ETR 可以独辟蹊径、跳过触发器直接接到触发控制器的原因------即在从模式控制器被占用时,还可以从 ETR 引入外部信号。
例子:TI1 门控 + ECE 计数 → 统计 TI1 高电平期间 ETR 来了几个脉冲。这样"从模式被占用时时钟走 ECE 旁路(TIM1 的内部触发连接(ITR0=TIM5、ITR1=TIM2、ITR2=TIM3、ITR3=TIM4,门模式:当输入信号为高电平(严格讲是检测到上升沿)时,门就打开,时钟信号可以进入到定时器,定时器正常计数;当输入信号为低电平(检测到下降沿)时,门就关闭,定时器暂停计数,输入通道的边沿检测器就可以改变高低电平对门的开关控制。)
(1.9)TI1FP1 与 TI2FP2 是什么?
我们知道触发器信号来源还有 TI1FP1 和 TI2FP2 这两个,那这两个是啥呢?
STM32 为每个通道都配套了输入滤波器和边沿检测器,边沿检测器可以检测输入信号的边沿并输出一个脉冲(可检测上升沿、下降沿、双边沿)。对于通道 1(TI1)来说,其边沿检测器输出的脉冲信号有两个(TI1FP1 和 TI1FP2),通道 2(TI2)也有两个(TI2FP1 和 TI2FP2)。
这里的命名规则是:TIxFPy 表示"通道 x 的输入信号,经过滤波和边沿检测后,送给触发器 y 的脉冲"。其中 FP 是 Filtered Pulse(滤波后的脉冲)的缩写。
- TI1FP1:通道 1 的输入信号经过滤波、边沿检测后产生的脉冲,送给触发器 1(TRGI 的 TI1FP1 输入)。
- TI2FP2:通道 2 的输入信号经过滤波、边沿检测后产生的脉冲,送给触发器 2(TRGI 的 TI2FP2 输入)。
所以 TI1FP1 和 TI2FP2 就是通道 1 和通道 2 经过滤波、边沿检测后输出的脉冲信号,它们可以作为外部时钟模式 1 的触发源,由 TIMx_SMCR 寄存器的 TS2:0 位选择。
(2)输入捕获:

输入捕获可以对输入的信号的上升沿、下降沿或者双边沿进行捕获,常用的有测量输入信号的脉宽和测量 PWM 输入信号的频率和占空比这两种。
输入捕获的大概的原理就是:当捕获到信号的跳变沿的时候,把计数器 CNT 的值锁存到捕获寄存器 CCR 中,把前后两次捕获到的 CCR 寄存器中的值相减,就可以算出脉宽或者频率。如果捕获的脉宽的时间长度超过你的捕获定时器的周期,就会发生溢出,这个我们需要做额外的处理。
(2.1)输入通道
需要被测量的信号从定时器的外部引脚 TIMx_CH1/2/3/4 进入,通常叫 TI1/2/3/4,在后面的捕获讲解中对于要被测量的信号我们都以 TIx 为标准叫法。
(2.2)输入滤波器和边沿检测器
当输入的信号存在高频干扰的时候,我们需要对输入信号进行滤波,即进行重新采样,根据采样定律,采样的频率必须大于等于两倍的输入信号。比如输入的信号为 1M,又存在高频的信号干扰,那么此时就很有必要进行滤波,我们可以设置采样频率为 2M,这样可以在保证采样到有效信号的基础上把高于 2M 的高频干扰信号过滤掉。
滤波器的配置由 CR1 寄存器的位 CKD1:0 和 CCMR1/2 的位 ICxF3:0 控制。从 ICxF 位的描述可知,采样频率 fSAMPLE 可以由 fCK_INT 和 fDTS 分频后的时钟提供,其中是 fCK_INT 内部时钟,fDTS是 fCK_INT 经过分频后得到的频率,分频因子由 CKD1:0 决定,可以是不分频,2 分频或者是 4分频。
边沿检测器用来设置信号在捕获的时候是什么边沿有效,可以是上升沿,下降沿,或者是双边沿,具体的由 CCER 寄存器的位 CCxP 和 CCxNP 决定。
(2.3)捕获通道
捕获通道就是图中的 IC1/2/3/4,每个捕获通道都有相对应的捕获寄存器 CCR1/2/3/4,当发生捕获的时候,计数器 CNT 的值就会被锁存到捕获寄存器中。
这里我们要搞清楚输入通道和捕获通道的区别,输入通道是用来输入信号的,捕获通道是用来捕获输入信号的通道,一个输入通道的信号可以同时输入给两个捕获通道(也就是TI1FP2和TI1FP2),这两个捕获通道就能帮我们解决了:如何一个输入捕获通道实现既能捕获上升沿,也能捕获下降沿的问题。
那怎么操作呢?例如:当定时器1通道(TIM1)捕获一个上升沿信号时,定时器就会将计数器的值转移到捕获寄存器1中,然后当定时器1通道(TIM1)捕获一个下降沿信号时,定时器就会将计数器的值转移到捕获寄存器2中,这时只要程序读取这两个寄存器的值,然后相减,就可以得到高电平的持续时间。
如果信号从TI1的GPIO口引入,则输入通道1(TI1)叫做输入捕获的直接模式,TI2叫做间接模式,反之同理,输入通道和捕获通道的映射关系具体由寄存器 CCMRx 的位 CCxS1:0 配置。
互为一对的输入通道,可以借用捕获寄存器进行间接捕获,TI1和TI2是一对,而一直被我们忽视的TI3和TI4也是一对,拥有和TI1与TI2一模一样的结构,唯一的区别是TI3FP3和TI4FP4没有接入到从模式控制器中(从FP后面的数字就可以看出谁和谁是一对的)
(2.4)预分频器
ICx 的输出信号会经过一个预分频器,用于决定发生多少个事件时进行一次捕获。具体的由寄存器 CCMRx 的位 ICxPSC 配置,如果希望捕获信号的每一个边沿,则不分频。
(2.5)捕获寄存器
经过预分频器的信号 ICxPS 是最终被捕获的信号,当发生捕获时(第一次),计数器 CNT 的值会被锁存到捕获寄存器 CCR 中,还会产生 CCxI 中断,相应的中断位 CCxIF(在 SR 寄存器中)会被置位,通过软件或者读取 CCR 中的值可以将 CCxIF 清 0。如果发生第二次捕获(即重复捕获:CCR 寄存器中已捕获到计数器值且 CCxIF 标志已置 1),则捕获溢出标志位 CCxOF(在 SR 寄存器中)会被置位,CCxOF 只能通过软件清零。
(3)输出比较

输出比较就是通过定时器的外部引脚对外输出控制信号,有冻结、将通道 X(x=1,2,3,4)设置为匹配时输出有效电平、将通道 X 设置为匹配时输出无效电平、翻转、强制变为无效电平、强制变为有效电平、PWM1 和 PWM2 这八种模式 ,具体使用哪种模式由寄存器 CCMRx 的位 OCxM2:0配置。其中 PWM 模式是输出比较中的特例,使用的也最多。
(3.1)比较寄存器**
**当计数器 CNT 的值跟比较寄存器 CCR 的值相等的时候,输出参考信号 OCxREF 的信号的极性就会改变,其中 OCxREF=1(高电平)称之为有效电平,OCxREF=0(低电平)称之为无效电平,并且会产生比较中断 CCxI,相应的标志位 CCxIF(SR 寄存器中)会置位。**然后 OCxREF 再经过一系列的控制之后就成为真正的输出信号 OCx/OCxN。
(3.2)死区发生器
在生成的参考波形 OCxREF 的基础上,可以插入死区时间,用于生成两路互补的输出信号 OCx和 OCxN ,死区时间的大小具体由 BDTR 寄存器的位 DTG7:0 配置。死区时间的大小必须根据与输出信号相连接的器件及其特性来调整。下面我们简单举例说明下带死区的 PWM 信号的应用,我们以一个板桥驱动电路为例。

在这个半桥驱动电路中,Q1 导通,Q2 截止,此时我想让 Q1 截止 Q2 导通,肯定是要先让 Q1 截止一段时间之后,再等一段时间才让 Q2 导通,那么这段等待的时间就称为死区时间,因为 Q1关闭需要时间(由 MOS 管的工艺决定)。如果 Q1 关闭之后,马上打开 Q2,那么此时一段时间内相当于 Q1 和 Q2 都导通了,这样电路会短路。
图带死区插入的互补输出 是针对上面的半桥驱动电路而画的带死区插入的 PWM 信号,图中的死区时间要根据 MOS 管的工艺来调节。

输出控制

在输出比较的输出控制中,参考信号 OCxREF 在经过死区发生器之后会产生两路带死区的互补信号 OCx_DT 和 OCxN_DT(通道 1~3 才有互补信号,通道 4 没有,其余跟通道 1~3 一样),这两路带死区的互补信号然后就进入输出控制电路,如果没有加入死区控制,那么进入输出控制电路的信号就直接是 OCxREF。
进入输出控制电路的信号会被分成两路,一路是原始信号,一路是被反向的信号,具体的由寄存器 CCER 的位 CCxP 和 CCxNP 控制。经过极性选择的信号是否由 OCx 引脚输出到外部引脚 CHx/CHxN 则由寄存器 CCER 的位 CxE/CxNE 配置。如果加入了断路(刹车)功能,则断路和死区寄存器 BDTR 的 MOE、OSSI 和 OSSR 这三个位会共同影响输出的信号。
(3.3)输出引脚
输出比较的输出信号最终是通过定时器的外部 IO 来输出的,分别为 CH1/2/3/4,其中前面三个通道还有互补的输出通道 CH1/2/3N。更加详细的 IO 说明还请查阅相关的数据手册。
(3.4)断路功能:

断路功能就是电机控制的刹车功能,使能断路功能时,根据相关控制位状态修改输出信号电平。在任何情况下,OCx 和 OCxN 输出都不能同时为有效电平,这关系到电机控制常用的 H 桥电路结构原因。
断路源可以是时钟故障事件,由内部复位时钟控制器中的时钟安全系统 (CSS) 生成,也可以是外部断路输入 IO,两者是或运算关系。系统复位启动都默认关闭断路功能,将断路和死区寄存器 (TIMx_BDTR) 的 BKE 为置 1,使能断路功能。可通过 TIMx_BDTR 寄存器的 BKP 位设置,设置断路输入引脚的有效电平,设置为 1 时输入 BRK 为高电平有效,否则低电平有效。
发送断路时,将产生以下效果:
• TIMx_BDTR 寄存器中主输出模式使能 (MOE) 位被清零,输出处于无效、空闲或复位状态;
• 根据相关控制位状态控制输出通道引脚电平;当使能通道互补输出时,会根据情况自动控制输出通道电平;
• 将 TIMx_SR 寄存器中的 BIF 位置 1,并可产生中断和 DMA 传输请求。
• 如果 TIMx_BDTR 寄存器中的自动输出使能 (AOE) 位置 1,则 MOE 位会在发生下一个 UEV事件时自动再次置 1。
(4)定时器初始化结构体详解
1.高级定时器-1 定时器基本初始化结构
c
typedef struct {
uint16_t TIM_Prescaler; // 预分频器
uint16_t TIM_CounterMode; // 计数模式
uint32_t TIM_Period; // 定时器周期
uint16_t TIM_ClockDivision; // 时钟分频
uint8_t TIM_RepetitionCounter; // 重复计算器
} TIM_TimeBaseInitTypeDef;
(1) TIM_Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器计数时钟 CK_CNT,它设定 PSC 寄存器的值。计算公式为:计数器时钟频率 (fCK_CNT) 等于 fCK_PSC / (PSC15:0 + 1),可实现 1 至 65536 分频。
(2) TIM_CounterMode:定时器计数方式,可设置为向上计数、向下计数以及中心对齐。高级控制定时器允许选择任意一种。
(3) TIM_Period:定时器周期,实际就是设定自动重载寄存器 ARR 的值,ARR 为要装载到实际自动重载寄存器(即影子寄存器)的值,可设置范围为 0 至 65535。
(4) TIM_ClockDivision:时钟分频,设置定时器时钟 CK_INT 频率与死区发生器以及数字滤波器采样时钟频率分频比。可以选择 1、2、4 分频。
(5) TIM_RepetitionCounter:重复计数器,只有 8 位,只存在于高级定时器。
2.高级定时器- 定时器输入捕获初始化
c
typedef struct {
uint16_t TIM_Channel; // 输入通道选择
uint16_t TIM_ICPolarity; // 输入捕获触发选择
uint16_t TIM_ICSelection; // 输入捕获选择
uint16_t TIM_ICPrescaler; // 输入捕获预分频器
uint16_t TIM_ICFilter; // 输入捕获滤波器
} TIM_ICInitTypeDef;
(1) TIM_Channel:捕获通道 ICx 选择,可选 TIM_Channel_1、TIM_Channel_2、TIM_Channel_3 或TIM_Channel_4 四个通道。它设定 CCMRx 寄存器 CCxS 位的值。
(2) TIM_ICPolarity:输入捕获边沿触发选择,可选上升沿触发、下降沿触发或边沿跳变触发。它设定 CCER 寄存器 CCxP 位和 CCxNP 位的值。
(3) TIM_ICSelection:输入通道选择,捕获通道 ICx 的信号可来自三个输入通道,分别为TIM_ICSelection_DirectTITIM_ICSelection_IndirectTI 或 TIM_ICSelection_TRC,具体的区别见图输入通道与捕获通道 IC 的映射 。它设定 CCRMx 寄存器的 CCxS1:0 位的值。

(4) TIM_ICPrescaler:输入捕获通道预分频器,可设置 1、2、4、8 分频,它设定 CCMRx 寄存器的 ICxPSC1:0 位的值。如果需要捕获输入信号的每个有效边沿,则设置 1 分频即可。
(5) TIM_ICFilter:输入捕获滤波器设置,可选设置 0x0 至 0x0F。它设定 CCMRx 寄存器 ICxF3:0位的值。一般我们不使用滤波器,即设置为 0。
3.高级定时器- 定时器输出比较初始化
c
typedef struct {
uint16_t TIM_OCMode; // 比较输出模式
uint16_t TIM_OutputState; // 比较输出使能
uint16_t TIM_OutputNState; // 比较互补输出使能
uint32_t TIM_Pulse; // 脉冲宽度
uint16_t TIM_OCPolarity; // 输出极性
uint16_t TIM_OCNPolarity; // 互补输出极性
uint16_t TIM_OCIdleState; // 空闲状态下比较输出状态
uint16_t TIM_OCNIdleState; // 空闲状态下比较互补输出状态
} TIM_OCInitTypeDef;
(1) TIM_OCMode:比较输出模式选择,总共有八种,常用的为 PWM1/PWM2。它设定 CCMRx 寄存器 OCxM2:0 位的值。
(2) TIM_OutputState:比较输出使能,决定最终的输出比较信号 OCx 是否通过外部引脚输出。它设定 TIMx_CCER 寄存器 CCxE/CCxNE 位的值。
(3) TIM_OutputNState: 比较互补输出使能,决定 OCx 的互补信号 OCxN 是否通过外部引脚输出。它设定 CCER 寄存器 CCxNE 位的值。
(4) TIM_Pulse:比较输出脉冲宽度,实际设定比较寄存器 CCR 的值,决定脉冲宽度。可设置范围为 0 至 65535。
(5) TIM_OCPolarity:比较输出极性,可选 OCx 为高电平有效或低电平有效(一般都设置高电平有效,低电平无效)。它决定着定时器通道有效电平。它设定 CCER 寄存器的 CCxP 位的值。
(6) TIM_OCNPolarity:比较互补输出极性,可选 OCxN 为高电平有效或低电平有效(一般都设置高电平有效,低电平无效)。它设定TIMx_CCER 寄存器的 CCxNP 位的值。
(7) TIM_OCIdleState:空闲状态时通道输出电平设置,可选输出 1 或输出 0,即在空闲状态(BDTR_MOE 位为 0) 时,经过死区时间后定时器通道输出高电平或低电平。它设定 CR2 寄存器的 OISx 位的值。
(8) TIM_OCNIdleState:空闲状态时互补通道输出电平设置,可选输出 1 或输出 0,即在空闲状态(BDTR_MOE 位为 0) 时,经过死区时间后定时器互补通道输出高电平或低电平,设定值必须与TIM_OCIdleState 相反。它设定是 CR2 寄存器的 OISxN 位的值。
4.高级定时器-断路和死区初始化结构
c
typedef struct {
uint16_t TIM_OSSRState; // 运行模式下的关闭状态选择
uint16_t TIM_OSSIState; // 空闲模式下的关闭状态选择
uint16_t TIM_LOCKLevel; // 锁定配置
uint16_t TIM_DeadTime; // 死区时间
uint16_t TIM_Break; // 断路输入使能控制
uint16_t TIM_BreakPolarity; // 断路输入极性
uint16_t TIM_AutomaticOutput; // 自动输出使能
} TIM_BDTRInitTypeDef;
(1) TIM_OSSRState:运行模式下的关闭状态选择,它设定 BDTR 寄存器 OSSR 位的值。
(2) TIM_OSSIState:空闲模式下的关闭状态选择,它设定 BDTR 寄存器 OSSI 位的值。
(3) TIM_LOCKLevel:锁定级别配置,BDTR 寄存器 LOCK1:0 位的值。
(4) TIM_DeadTime:配置死区发生器,定义死区持续时间,可选设置范围为 0x0 至 0xFF。它设定BDTR 寄存器 DTG7:0 位的值。
(5) TIM_Break:断路输入功能选择,可选使能或禁止。它设定 BDTR 寄存器 BKE 位的值。
(6) TIM_BreakPolarity:断路输入通道 BRK 极性选择,可选高电平有效或低电平有效。它设定BDTR 寄存器 BKP 位的值。
(7) TIM_AutomaticOutput:自动输出使能,可选使能或禁止,它设定 BDTR 寄存器 AOE 位的值。