嵌入式课程学习_作业7

前言

本次作业是有关于第八章和第十章的内容,干脆合在一起做了,有可能这是最后一次作业了(问为什么没有第九章的内容?因为不讲这一章),第八章的内容是有关于flash编程以及ADC和DAC的,然后第十章是有关于CAN总线,还是老样子,在开始作业之前,我们先简单来过一过这个内容,然后做一下作业吧。

学习

第八章内容

Flash在线编程

先简要介绍一下flash存储器的特点

  • 失电后不丢失信息
  • 电可擦除
  • 可在线编程
  • 存储密度高
  • 功耗低和成本较低

flash存储器的擦写的两种模式

  • 写入器编程模式,即通过编程器将程序写入Flash存储器中,一般用于初始程序的写入
  • 在线编程模式,即通过运行Flash内部程序对Flash其他区域进行擦除与写入,用于程序运行过程中,进行部分程序的更新或保存数据

简要了解一下信息就可以了,我们主要还是通过参考手册代码中学习如何使用这个flash,首先要学习之前,我们看一下我们自己用的芯片的flash的地址范围是什么

如上图,我们可以看到实际上这个flash就在我们的0x0800 0000~0x0803 FFFF处,换算一下,可以看到是一共是有256kb,实际上这256kb是由128个扇区,每个扇区占2字节。在线编程时,擦除以扇区为单位进行,写入以字(4个字节)为单位,写入首地址需字对齐,介绍完这些之后,我们不妨打开书本看看对于寄存器的介绍,如下

我们不妨打开参考手册来看看关于关于这些寄存器的解释,以下是它们的map,关于每一个的解释,我们就不一一说明了

可以看到,关于Flash的寄存器组大概内容就是这些了,那么接下来我们打开我们的例子工程进行学习一下,我们这里主要理解一下flash.c文件的内容

//======================================================================
//函数名称:flash_init
//函数返回:无
//参数说明:无
//功能概要:初始化flash模块
//编程来源:暂无
//======================================================================
void flash_init(void)
{
    //(1)清除之前的编程导致的所有错误标志位
    FLASH->SR &= 0xFFFFFFUL;
    //(2)解锁Flash控制寄存器(CR)
    if((FLASH->CR & FLASH_CR_LOCK) != 0U)
    {
        FLASH->KEYR = (uint32_t)FLASH_KEY1;
        FLASH->KEYR = (uint32_t)FLASH_KEY2;
    }
    //(3)等待解锁操作成功
    while((FLASH->CR & FLASH_CR_LOCK) != 0U);
    //(4)等待之前最后一个flash操作完成
    while( (FLASH->SR & FLASH_SR_BSY) != 0U);
    //(5)清数据缓冲区
    FLASH->ACR &= ~FLASH_ACR_DCEN_Msk;
    //(6)清闪存即时编程位
    FLASH->CR &= ~FLASH_CR_PG_Msk;
}

可以看到,跟之前我们学的都大差不差,就是清空什么标志位啊,把什么使能打开啊,这些都是一样的,特别的是,这里值得注意的是,这个Flash跟之前的RTC时钟一样,实际上是有密码保护的,最关键的是,这里居然还要设置两个密码,我们查找一下数据手册,能够发现密码如下

那么关于init的理解和分析就到这里

//======================================================================
//函数名称:flash_erase
//函数返回:函数执行执行状态:0=正常;1=异常。
//参数说明:sect:目标扇区号(范围取决于实际芯片,例如 STM32L433:0~127,每扇区2KB;
//功能概要:擦除flash存储器的sect扇区
//编程参考:STM32L4Rxxx芯片手册3.3.6主存储器擦除顺序
//======================================================================
uint8_t flash_erase(uint16_t sect)
{
    
    //(1)等待之前最后一个flash操作完成
    while( (FLASH->SR & FLASH_SR_BSY) != 0U);
    //(2)清除之前的编程导致的所有错误标志位
    FLASH->SR &= 0xFFFFFFUL;
    //(3)清闪存即时编程位,将 PER 位置 1,并选择要擦除的页 (PNB)和相关存储区 (BKER)
    FLASH->CR &= ~FLASH_CR_PG;
    FLASH->CR |= FLASH_CR_PER;
    FLASH->CR &= ~FLASH_CR_PNB;
    FLASH->CR |= (uint32_t)(sect << 3u);
    //(4)将 FLASH_CR 寄存器中的 STRT 位置 1,开始扇区擦除
    FLASH->CR |= FLASH_CR_STRT;
    //(5)等待 FLASH_SR 寄存器中的 BSY 位清零,等待擦除操作完成
    while( (FLASH->SR & FLASH_SR_BSY) != 0U);
    FLASH->CR &= ~FLASH_CR_PER;
    return 0;  //成功返回
}

我们可以看到,实际上我们可以通过这个顺序进行页面的擦除,具体的方式在参考手册中有详细的说明,如下

接下来是逻辑写的函数,如下

//======================================================================
//函数名称:flash_write
//函数返回:函数执行状态:0=正常;1=异常。
//参数说明:sect:扇区号(范围取决于实际芯片,例如 STM32L433:0~127,每扇区2KB)
//        offset:写入扇区内部偏移地址(0~2044,要求为0,4,8,12,......)
//        N:写入字节数目(4~2048,要求为4,8,12,......)
//        buf:源数据缓冲区首地址
//功能概要:将buf开始的N字节写入到flash存储器的sect扇区的 offset处
//编程参考:暂无
//=======================================================================
uint8_t flash_write(uint16_t sect,uint16_t offset,uint16_t N,uint8_t *buf)
{
    //(1)定义变量
    uint16_t i;
    //(2)清除之前的编程导致的所有错误标志位
    FLASH->SR &= 0xFFFFFFUL;
    //(3.1)写入字节数后会跨扇区
    if(offset+N>MCU_SECTORSIZE)
    {
        //(3.1.1)先写入第一个扇区
        flash_write(sect,offset,MCU_SECTORSIZE-offset,buf);
        //(3.1.2)再写入第二个扇区
        flash_write(sect+1,0,N-(MCU_SECTORSIZE-offset),buf+(MCU_SECTORSIZE-offset));
    }
    //(3.2)写入字节数不会跨扇区
    else
    {
            uint8_t data[MCU_SECTORSIZE]; //存储当前扇区的全部值
            flash_read_logic(data,sect,0,MCU_SECTORSIZE); //将当前扇区的全部值读入数组中
            //将要写入的数据依照对应位置写入数组中
            for(i = 0;i<N;i++)
            {
                data[offset+i] = buf[i];
            }
            //擦除扇区
            flash_erase(sect);
            //将数组写入扇区
            flash_Best(sect,0,MCU_SECTORSIZE,data);
    }
    //(4)等待写入操作完成
    while( (FLASH->SR & FLASH_SR_BSY) != 0U);
    return 0;  //成功执行
}

这段代码用于写入数据到STM32微控制器上的Flash存储器。它按照指定的扇区号、偏移量和字节数,将一个数据缓冲区(buf)中的内容写入到Flash中,判断有无超越边界,即一个扇区装不装的完,装不完则递归调用写入,如果可以,就先擦除,再写入

接下来是物理写的函数,如下

//==========================================================================
//函数名称:flash_write_physical
//函数返回:函数执行状态:0=正常;非0=异常。
//参数说明: addr:目标地址,要求为4的倍数且大于Flash首地址
//              (例如:0x08000004,Flash首地址为0x08000000)
//       cnt:写入字节数目(8~512)
//       buf:源数据缓冲区首地址
//功能概要:flash写入操作
//编程参考:暂无
//==========================================================================
uint8_t flash_write_physical(uint32_t addr,uint16_t N,uint8_t buf[])
{
    //(1)定义变量。sect-扇区号,offset-扇区地址
    uint16_t sect;   //扇区号
    uint16_t offset;    // 偏移地址
    //(2)变量赋值,将物理地址转换为逻辑地址(扇区和偏移量)
    sect = (addr-MCU_FLASH_ADDR_START)/MCU_SECTORSIZE;
    offset = addr-(sect*MCU_SECTORSIZE)-MCU_FLASH_ADDR_START;
    //(3)调用写入函数写入数据
    flash_write(sect,offset,N,buf);
    //(4)等待写入操作完成
    while( (FLASH->SR & FLASH_SR_BSY) != 0U);
    return 0;  //成功执行
}

实际上这一段代码一样的,对输入的参数进行解析,将其转换为对应的扇区号和偏移地址,在调用逻辑写函数而已

接下来是物理读的函数

//======================================================================
//函数名称:flash_read_logic
//函数返回:无
//参数说明:dest:读出数据存放处(传地址,目的是带出所读数据,RAM区)
//       sect:扇区号(范围取决于实际芯片,例如 STM32L433:0~127,每扇区2KB)
//       offset:扇区内部偏移地址(0~2024,要求为0,4,8,12,......)
//       N:读字节数目(4~2048,要求为4,8,12,......)//
//功能概要:读取flash存储器的sect扇区的 offset处开始的N字节,到RAM区dest处
//编程参考:暂无
//=======================================================================
void flash_read_logic(uint8_t *dest,uint16_t sect,uint16_t offset,uint16_t N)
{
    //(1)定义变量。src-读取的数据的地址
    uint8_t *src;
    //(2)变量赋值。通过扇区号和偏移量计算出逻辑地址
    src=(uint8_t *)(FLASH_BASE+sect*FLASH_PAGE_SIZE+offset);
    //(3)读出数据
    memcpy(dest,src,N);
}

这一段代码就很简单了,主要目的就是通过传入的参数获取逻辑地址,然后通过memcpy读取N个字节数,至于下面还有一个物理读的函数,则不需要通过扇区号和偏移量计算出地址,直接传入物理地址就可以了

接下来是判断flash中指定区域是否为空的函数

//======================================================================
//函数名称:flash_isempty
//函数返回:1=目标区域为空;0=目标区域非空。
//参数说明:所要探测的flash区域初始地址
//功能概要:flash判空操作
//编程来源:暂无
//======================================================================

uint8_t flash_isempty(uint16_t sect,uint16_t N)
{
    //(1)定义变量flag-操作成功标志,src-目标地址,dest[]-暂存数据
    uint16_t i,flag;
    uint8_t dest[N];
    uint8_t *src;
    //(2)变量赋值并读取数据
    flag = 1;
    src=(uint8_t *)(FLASH_BASE+sect*FLASH_PAGE_SIZE);
    memcpy(dest,src,N);
    //(3)判断区域内数据是否为空
    for(i = 0; i<N; i++)   //遍历区域内字节
    {
        if(dest[i]!=0xff)   //非空
        {
            flag=0;
            break;
        }
    }
    return flag;
}

从这段我们可以发现,一般来说,默认下清空下的所有字节的值就是0xFF,函数通过循环区域内字节,看看有没有不是0xFF的值,如果有,那么就说明该区域非空

那么Flash在线编程就先学到这里吧,接下来跑一跑示例程序,首先,示例程序大概就是利用逻辑写,逻辑读,物理写,物理读的方式进行Flash的读写,大体如下

上面的函数在上面都有简单的介绍,下面是运行结果

可以看到,运行结果还是挺正常的

ADC&DAC

模拟量(Analogue quantity)是指变量在一定范围连续变化的物理量,从数学角度,连续变化可以理解为可取任意值。例如,温度这个物理量,可以有28.1℃,也可以有28.15℃,还可以有28.152℃,….,也就是说,原则上可以有无限多位小数点,这就是模拟量连续之含义。当然实际达到多少位小数点则取决于问题需要与测量设备性能。

数字量(Digital Quantity)是分立量,不可连续变化只能取一些分立值。在计算机中,所有信息均使用二进制表示。例如,用一位只能表达0、1两个值,8位可以表达0、1、2、254、255,共256个值,不能表示其他值,这就是数字量。

什么是ADC?指的是“模数转换器”(Analog-to-Digital Converter),它是一种电子装置,用于将模拟信号(连续的信号)转换为数字信号(离散的信号)。在数字电路系统中,模拟信号需要被转换为数字信号才能被处理,ADC就是这个转换过程的关键组件。例如,在音频处理、测量仪器和通信设备中,模拟信号(如声音、温度、压力等)通常需要通过ADC转换为数字信号,以便数字信号处理器(如计算机)能够进行进一步的处理。

什么是DAC?指的是“数模转换器”,Digital-to-Analog Converter)是一种电子电路,它的功能是将数字信号(通常是二进制)转换为模拟信号。在数字设备(如计算机、手机和数字音频播放器)和模拟设备(如扬声器、显示器和其他模拟电路)之间的接口中,DAC扮演着至关重要的角色。数字信号是由0和1组成的离散信号,而模拟信号是连续的,可以取无限多个值。为了能够用模拟设备(如扬声器)播放数字音频文件,或者用显示器显示数字图像,就需要将数字信号转换为模拟信号。DAC就是完成这一转换的设备。

我们直接看一下adc_init函数,如下

//======================================================================
//函数名称:adc_init
//功能概要:初始化一个AD通道号与采集模式
//参数说明:Channel:通道号;可选范围:ADC_CHANNEL_VREFINT、
//         ADC_CHANNEL_TEMPSENSOR、ADC_CHANNEL_x(1=<x<=16)、ADC_CHANNEL_VBAT
//         diff:差分选择。=1(AD_DIFF 1),差分;=0(AD_SINGLE);
//         单端;ADC_CHANNEL_VREFINT、ADC_CHANNEL_TEMPSENSOR、ADC_CHANNEL_VBAT
//         强制为单端;ADC_CHANNEL_x(1=<x<=16)可选单端或者差分模式
//======================================================================

void adc_init(uint16_t Channel,uint8_t Diff)
{
    //(1)开启ADC时钟,频率=总线时钟/4,48MHz,ADC时钟不超过14MHz
    RCC->AHB2ENR |= RCC_AHB2ENR_ADCEN;
    ADC1_COMMON->CCR |= ADC_CCR_CKMODE; //4分频

    //(2)退出掉电状态并使能稳压器,ADC默认处于掉电状态以降低功耗
    //【20220614修改】
    if(Diff == AD_DIFF)
    {
    	ADC1->CR |= (1<<30);       //在差分输入模式下启动校准
    }else
    {
    	ADC1->CR &= ~(1<<30);      //在单端输入模式下启动校准
    }
    ADC1->CR &= ~ADC_CR_DEEPPWD;
    ADC1->CR |= 1<<31;             //开始校准
    ADC1->CR |= ADC_CR_ADVREGEN;   
    for(volatile uint32_t i = 0;i<48000;i++) //延时1毫秒以稳定
    //(3)使能ADC相关内部采集功能
    if(Channel == 0)
    {
        ADC1_COMMON->CCR |= ADC_CCR_VREFEN;  //使能参考电压采集功能
    }
    else if(Channel == 17)
    {
        ADC1_COMMON->CCR |= ADC_CCR_TSEN;    //使能温度采集功能
    }
    else if(Channel == 18)
    {
        ADC1_COMMON->CCR |= ADC_CCR_VBATEN;  //使能基准电压采集功能
    }
    
    //(4)初始化ADC控制寄存器,清零各个控制位
    ADC1->CR &= 0x3fffffc0;
    //(5)单端差分选择
    if (Diff)
    {
        BSET(Channel,ADC1->DIFSEL);  
    }
    else
    {
        BCLR(Channel,ADC1->DIFSEL);	
    }
    
    //(6)开启ADC
    ADC1->CR |= ADC_CR_ADEN;
    
    //(7)设置采样时间为12.5个时钟周期
    if((int)Channel >= 0 && (int)Channel <= 9 )
    {
        BCLR(Channel*3,ADC1->SMPR1);
        BSET(Channel*3+1,ADC1->SMPR1);
        BCLR(Channel*3+2,ADC1->SMPR1);
    }
    if((int)Channel >= 10 && (int)Channel <= 18 )
    {
        BCLR((Channel%10)*3,ADC1->SMPR2);
        BSET((Channel%10)*3+1,ADC1->SMPR2);
        BCLR((Channel%10)*3+2,ADC1->SMPR2);
    }
    //(8)配置寄存器CFGR:精度12位、右对齐、单次单通道转换
    ADC1->CFGR &= 0xffffffe7;           //精度设置为12位
    ADC1->CFGR &= 0xffffffdf;           //数据对齐方式右对齐
    ADC1->CFGR |= ADC_CFGR_DISCEN;      //不连续转换模式
    ADC1->CFGR &= ~ADC_CFGR_CONT;       //单次转换模式
    ADC1->CFGR &= ~ADC_CFGR_DISCNUM;    //一个通道
    ADC1->CFGR &= ~ADC_CFGR_EXTEN;      //禁止硬件触发检测
    //(9)常规通道序列长度为1
    ADC1->SQR1 &= ~ADC_SQR1_L;	   
}

可以看到一般来说adc的初始化包括:

  • 开启ADC时钟:
    • 使能ADC时钟,通过设置RCC->AHB2ENR寄存器的ADCEN位。
    • 配置ADC时钟分频,通过ADC1_COMMON->CCR寄存器的CKMODE位,确保ADC时钟不超过14MHz。
  • 退出掉电状态并使能稳压器:
    • 根据差分模式配置校准,通过ADC1->CR寄存器的DEEPPWD和ADVREGEN位。
    • 如果需要,启动ADC校准过程,通过ADC1->CR寄存器的ADCAL位,并等待校准完成。
  • 使能ADC相关内部采集功能:
    • 根据通道号使能特定的内部采集功能,如参考电压、温度传感器或基准电压,通过ADC1_COMMON->CCR寄存器。
  • 初始化ADC控制寄存器:
    • 清零ADC1->CR寄存器中的各个控制位,以配置ADC的工作模式。
  • 单端差分选择:
    • 根据差分模式选择,通过ADC1->DIFSEL寄存器配置单端或差分模式。
  • 开启ADC:
    • 通过ADC1->CR寄存器的ADEN位使能ADC。
  • 设置采样时间:
    • 通过ADC1->SMPR1和ADC1->SMPR2寄存器设置ADC通道的采样时间。
  • 配置寄存器CFGR:
    • 配置ADC的精度(12位)、数据对齐方式(右对齐)、转换模式(单次或连续)、硬件触发等,通过ADC1->CFGR寄存器。
  • 常规通道序列长度配置:
    • 通过ADC1->SQR1寄存器配置常规通道序列的长度,即转换序列中包含的通道数。

接下来我们测试一下示例程序,示例程序实际上是测试了三个东西,一个是内部温度传感器的单端测试,其他两个是通道1和通道15的差分测试,我们编译程序,试着运行一下,可以看到如下运行结果

可以看到,检测出了某一时刻的三个通道的AD值,接着我们试着把我们的手放到芯片上捂住,可以观察到内部温度传感器的AD值在上升,如下

可以看到,内部温度传感器的AD值又873上升到了887

OK,接下来我们测试一下通道1,从代码中其实我们知道,当通道号选择1~16,这里测出来的AD值其实就是两个引脚之间的电压的AD值,就拿上一个图来看,通道1的AD值是2385,参考电压是3.3V,然后精度为2^12即4096,那么该时刻实际上引脚47,46之间的电压就是2385/4096*3.3≈1.92V,接下来我们试着按照刚开始那样用杜邦线将两个引脚分别接3.3V和接地,看看效果,首先是47接地

 

检测结果如下

可以看到,这里的AD值为0,那当我们反着来,将46接地,如下

检测结果如下

可以看到AD值为4031,接近满量程

分析:为什么接反了,会有0和4031的表现?

差分ADC有两个输入引脚:正输入(通常标记为IN+或AIN+)和负输入(通常标记为IN-或AIN-)。ADC测量的是这两个引脚之间的电压差。我们测的电压根据方向的不同实际上会有正负之分,显示0的那种情况很可能是测到了-3.3V,然后由于ADC可能不支持测量负电压,因此显示0。

然后我们来分析一下使用通道15的差分输出,这个通道15实际上有点好玩,因为这两个引脚中间弄了一个热敏电阻,所以实际上我们能对热敏电阻进行操作改变其电压,如下图,某一时刻,通道15的AD值为2559,如下

接下来我们试着用手指按住热敏电阻,使其升温

此时检测结果如下

可以看到,实际上当我们把用手捂住了热敏电阻之后,AD明显下降了,这是因为电阻受热,电阻降低,因此电压就变小,也就是说,实际上得知了热敏电阻的更为详细的信息,实际上我们能用他来检测环境的温度,这个我们下面作业中会提及。

接下来讲一下DAC吧,不过DAC没有什么好说的,跟ADC反着来嘛,我们看看示例工程的相关函数的流程

//======================================================================
//函数名称:dac_init
//功能概要:初始化DAC模块设定
//参数说明:port_pin:可选择DAC_PIN1代表通道1
//                    选择DAC_PIN2代表通道2
//======================================================================
void dac_init(uint16_t port_pin)
{
        //(1)开启DAC1时钟
		RCC->APB1ENR1 |= RCC_APB1ENR1_DAC1EN;
		//(2)选择使用DAC1的通道1或者通道2
		if(port_pin==1)
		{
		    //DAC1通道1不使用触发功能,不生成波
		    DAC1->CR &= ~DAC_CR_TEN1; //不使用触发功能
		    DAC1->CR &= ~DAC_CR_WAVE1_0; //不生成波
            //DAC1通道1输出缓存关闭
		    DAC1->MCR|=DAC_MCR_MODE1_2;
            //DAC1通道1软件触发
		    DAC1->SWTRIGR|= DAC_SWTRIGR_SWTRIG1;
            //DAC1通道1启动
		    DAC1->CR|=DAC_CR_EN1;
		}
		else if(port_pin==2)
		{
		    //DAC1通道2不使用触发功能,不生成波
		    DAC1->CR &= ~DAC_CR_TEN2; //不使用触发功能
		    DAC1->CR &= ~DAC_CR_WAVE2_0; //不生成波
            //DAC1通道2输出缓存关闭
		    DAC1->MCR|=DAC_MCR_MODE2_2;
            //DAC1通道2软件触发
		    DAC1->SWTRIGR|= DAC_SWTRIGR_SWTRIG2;
            //DAC1通道2启动
		    DAC1->CR|=DAC_CR_EN2;
		}

}

总体而言,大致流程如下

  • 开启DAC时钟
  • 选择DAC通道
  • 配置DAC通道特性,例如关闭触发功能,关闭波形生成功能,关闭输出缓存,配置软件触发,启动DAC通道

然后是进行DAC转换的函数

//============================================================================
//函数名称:dac_convert
//功能概要:执行DAC转换
//参数说明:port_pin:可选择DAC_PIN1代表通道1
//                   选择DAC_PIN2代表通道2
//          data:需要转换成数字量的模拟量:范围(0~4095)
//============================================================================
void dac_convert(uint16_t port_pin,uint16_t data)
{
    //(1)开启DAC1
    if(port_pin==1)
    {
        //开启DAC1通道1
        DAC1->CR|=DAC_CR_EN1;
        //通道1传递输入数值
	   	DAC1->DHR12R1 = data;
    }
	else if(port_pin==2)
	{
	    //开启DAC1通道2
        DAC1->CR|=DAC_CR_EN2;
        //通道1传递输入数值
	   	DAC1->DHR12R2 = data;
	}
	
}

流程如下

  • 开启指定的DAC通道,对于选定的通道,开启使能
  • 写入数据到DAC数据保持寄存器,将12位的数字数据写入到相应的DAC数据保持寄存器中,以开始转换过程

大概就是这样,接下来我们来运行一下示例工程

其中PTA4会每一次循环都会增加100的AD值

工程大体就是使用引脚40作为DAC功能,然后使其输出模拟电压,然后利用单端的ADC检测这个模拟电压并输出,运行如下

可以看到,这个DAC输出每次是差不多递增100的,验证成功!

那么关于第八章的内容大概就到这里了!下面进入第十章的内容

第十章内容

CAN总线

先简要介绍一下CAN总线的一些特点,然后对比一下其他我们熟知的一些通信方式的区别

CAN总线(Controller Area Network)是一种为车辆应用设计的多主通信总线,由Robert Bosch GmbH在1983年开发,主要用于汽车电子环境中无需主机计算机的串行通信。由于其高可靠性、实时性和灵活性,CAN总线被广泛应用于汽车、工业自动化、医疗设备等多个领域。

  • 多主架构:CAN总线是多主架构的,这意味着网络上的每个节点都可以自由地发送和接收数据,而无需中央控制器。
  • 高可靠性:CAN总线采用非破坏性仲裁机制和错误检测机制(包括循环冗余校验CRC、位填充、确认应答等),确保数据传输的高可靠性。
  • 数据速率:低速CAN通信速率10-125Kbps,总线长度可达1000米。典型的CAN总线数据速率范围为125 kbps到1 Mbps,总线长度最高可达40米。特定版本如CAN FD(Flexible Data-rate)则支持更高的数据速率。
  • 数据帧格式:CAN数据帧长度为8字节,可以传输多种数据类型,并支持标准帧(11位标识符)和扩展帧(29位标识符)。

CAN vs UART

  • 通信模式:
    • CAN:多主总线,支持多节点之间的通信,适用于复杂的多节点系统。
    • UART:点对点通信,通常用于两个设备之间的直接通信。
  • 同步方式:
    • CAN:同步通信,所有节点在同一时钟源下运行。
    • UART:异步通信,发送方和接收方有独立的时钟源。
  • 数据速率:
    • CAN:最高可达1 Mbps(传统CAN),更高的数据速率可通过CAN FD实现。
    • UART:数据速率一般为几百kbps,视硬件和系统要求而定。
  • 错误检测与处理:
    • CAN:内置强大的错误检测机制(例如CRC、位填充、确认应答等)。
    • UART:基本的错误检测机制(如奇偶校验、帧错误检测等),通常需要软件实现更多的错误处理。

接下来我们看看CAN一个标准数据帧的例子,如下

由于本身CAN总线的结构非常复杂,要我们全部写出来过一篇不太现实,因次我们选择接下来的什么控制器啊,寄存器啊,模式等,都基于我们工程代码的构件函数进行分析,那么话不多说,直接开始分析,下面是can.c,我们一个函数块一个函数块进行分析

我要做什么?第一,将源can.c文件下的标有//2024.6附近的语句块进行注释补全,第二,给出该代码块主要用于做什么(刚好对应PPT要求)

//=====================================================================
// 函数名称:can_init
// 函数返回:无
// 参数说明:canNo:模块号,本芯片只有CAN_1
//		    canID:自身CAN节点的唯一标识,例如按照CANopen协议给出
//          BitRate:位速率
// 功能概要:初始化CAN模块
//=====================================================================
void can_init(uint8_t canNo, uint32_t canID, uint32_t BitRate)
{
    // 声明Init函数使用的局部变量
    uint32_t CANMode;
    uint32_t CANFilterBank;
    uint32_t CANFiltermode;
    uint32_t CAN_Filterscale;

    // 给Init函数使用的局部变量赋初值
    CANMode = CAN_MODE_NORMAL;               // 设置CAN总线工作在正常模式
    CANFilterBank = CANFilterBank0;          // 设置CAN总线过滤器组0
    CANFiltermode = CAN_FILTERMODE_IDMASK;   // 设置CAN总线过滤器模式为ID掩码模式
    CAN_Filterscale = CAN_FILTERSCALE_32BIT; // 设置CAN总线过滤器位宽为32位

    // (1)CAN总线硬件初始化
    CAN_HWInit(CAN_CHANNEL);
    // (2)CAN总线进入软件初始化模式
    CAN_SWInit_Entry(canNo);
    // (3)CAN总线模式设置
    CAN_SWInit_CTLMode(canNo);
    // (4)CAN总线位时序配置
    CAN_SWInit_BT(canNo, CANMode, BitRate);
    // (5)CAN总线过滤器初始化
    CANFilterConfig(canNo, canID, CANFilterBank, CAN_RX_FIFO0, 1, CANFiltermode, CAN_Filterscale);
    // (6)CAN总线退出软件初始化模式,进入正常模式
    CAN_SWInit_Quit(canNo);
}

解析:这段代码按步骤对CAN总线模块进行初始化。它设置了CAN总线的工作模式、过滤器、位速率,并确保总线处于正常工作状态。每一步骤对应相应的硬件和软件配置,确保CAN模块能正确通信。

//=====================================================================
//函数名称:can_send
//函数返回:0=正常,1=错误
//参数说明:canNo:模块号,本芯片只有CAN_1
//          DestID:目标CAN节点的唯一标识,例如按照CANopen协议给出
//          len:待发送数据的字节数
//          buff:待发送数据发送缓冲区首地址
//功能概要:CAN模块发送数据
//=====================================================================
uint8_t can_send(uint8_t canNo, uint32_t DestID, uint16_t len ,uint8_t* buff)
{
	if(DestID > 0x1FFFFFFFU) return 1;
	uint8_t send_length;
    //这个循环用来将数据按8字节一组分段发送
	for(int i = len; i > 0; i = i-8)
	{
		send_length = (i>8)?8:i;//确定每次发送的数据长度。如果剩余数据大于8字节,则发送8字节,否则发送剩余的所有数据
		if(can_send_once(canNo,DestID,send_length,buff+len-i) == 1)   //调用 can_send_once 函数发送一次数据
		{
			return 1;
		}
	}
	return 0;
}

函数用于通过CAN总线发送数据。它首先检查目标ID是否在合法范围内,然后将数据按最多8字节一组进行分段发送。每一段数据发送时,调用can_send_once函数进行实际发送。如果发送过程中有任何一段失败,函数返回1表示错误;否则,返回0表示发送成功。

//=====================================================================
//函数名称:can_recv
//函数返回:接收到的字节数
//参数说明:canNo:模块号,本芯片只有CAN_1
//          buff:接收到的数据存放的内存区首地址
//功能概要:在CAN模块接收中断中调用本函数接收已经到达的数据
//=====================================================================
uint8_t can_recv(uint8_t canNo, uint8_t *buff)
{
	uint8_t len;
	uint32_t RxFifo = CAN_RX_FIFO0;
	//(1)判断哪个邮箱收到了报文信息
	if(RxFifo == CAN_RX_FIFO0)
	{
		if ((CAN_ARR[canNo-1]->RF0R & CAN_RF0R_FMP0) == 0U)   //CAN_RF0R_FMP0 是CAN接收FIFO0的消息待处理标志位,这里操作判断哪个邮箱收到了报文信息
		{
			return 1;
		}
	}
	else
	{
		if ((CAN_ARR[canNo-1]->RF1R & CAN_RF1R_FMP1) == 0U)
		{
			return 1;
		}
	}
	//(2)获取数据长度
    len = (CAN_RDT0R_DLC & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDTR) >> CAN_RDT0R_DLC_Pos;  //CAN_RDT0R_DLC是CAN接收数据长度码位掩码,CAN_RDT0R_DLC_Pos是数据长度码的位置,通过右移操作得到数据长度
    //(3)获取数据帧中的数据
    buff[0] = (uint8_t)((CAN_RDL0R_DATA0 & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDLR) >> CAN_RDL0R_DATA0_Pos);
    buff[1] = (uint8_t)((CAN_RDL0R_DATA1 & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDLR) >> CAN_RDL0R_DATA1_Pos);
    buff[2] = (uint8_t)((CAN_RDL0R_DATA2 & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDLR) >> CAN_RDL0R_DATA2_Pos);
    buff[3] = (uint8_t)((CAN_RDL0R_DATA3 & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDLR) >> CAN_RDL0R_DATA3_Pos);
    buff[4] = (uint8_t)((CAN_RDH0R_DATA4 & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDHR) >> CAN_RDH0R_DATA4_Pos);
    buff[5] = (uint8_t)((CAN_RDH0R_DATA5 & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDHR) >> CAN_RDH0R_DATA5_Pos);
    buff[6] = (uint8_t)((CAN_RDH0R_DATA6 & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDHR) >> CAN_RDH0R_DATA6_Pos);
    buff[7] = (uint8_t)((CAN_RDH0R_DATA7 & CAN_ARR[canNo-1]->sFIFOMailBox[RxFifo].RDHR) >> CAN_RDH0R_DATA7_Pos);
    //(4)清除标志位,等待接收下一帧数据
    if (RxFifo == CAN_RX_FIFO0)
    {
      SET_BIT(CAN_ARR[canNo-1]->RF0R, CAN_RF0R_RFOM0);  //设置RF0R寄存器中的RFOM0位,以释放FIFO0的输出邮箱,准备接收下一条消息
    }
    else
    {
      SET_BIT(CAN_ARR[canNo-1]->RF1R, CAN_RF1R_RFOM1);
    }
	return len;
}

函数用于在CAN总线接收中断中接收数据。它首先检查接收邮箱中是否有新消息,如果没有则返回1表示错误。如果有新消息,则提取消息的数据长度和数据内容,并将数据存储到缓冲区buff中。最后清除接收标志位,准备接收下一帧数据,并返回接收到的数据长度。

//=====================================================================
//函数名称:CAN_enable_re_int
//函数返回:无
//参数说明:canNo:模块基地址号,Can_Rx_FifoNo:中断使用的邮箱号
//功能概要:CAN接收中断开启
//=====================================================================
void can_enable_recv_int(uint8_t canNo)
{
	uint8_t Can_Rx_FifoNo;
	Can_Rx_FifoNo = CAN_RX_FIFO0;
	if(Can_Rx_FifoNo == CAN_RX_FIFO0)
		SET_BIT(CAN_ARR[canNo-1]->IER, CAN_IER_FMPIE0);    //设置CAN_IER寄存器中的FMPIE0位,启用FIFO0的接收消息挂号中断
	else
		SET_BIT(CAN_ARR[canNo-1]->IER,CAN_IER_FMPIE1);      //设置CAN_IER寄存器中的FMPIE1位,启用FIFO1的接收消息挂号中断
	NVIC_EnableIRQ(table_irq_can[Can_Rx_FifoNo]);     //用于使能与接收FIFO对应的NVIC中断请求
}

这段代码的功能是启用指定 CAN 模块的接收中断。

//=====================================================================
//函数名称:can_disable_recv_int
//函数返回:无
//参数说明:canNo:模块号,本芯片只有CAN_1
//功能概要:关闭CAN接收中断
//=====================================================================
void can_disable_recv_int  (uint8_t canNo)
{
	uint8_t Can_Rx_FifoNo;
	Can_Rx_FifoNo = CAN_RX_FIFO0;
	if(Can_Rx_FifoNo == CAN_RX_FIFO0)
		CLEAR_BIT(CAN_ARR[canNo-1]->IER, CAN_IER_FMPIE0);
	else
		CLEAR_BIT(CAN_ARR[canNo-1]->IER,CAN_IER_FMPIE1);
	NVIC_DisableIRQ(table_irq_can[Can_Rx_FifoNo]);
}

这段代码跟上一段相反,使能中断关闭

//=====================================================================
//函数名称:can_send_once
//函数返回:0=正常,1=错误
//参数说明:canNo:模块号,本芯片只有CAN_1
//          DestID:目标CAN节点的唯一标识,例如按照CANopen协议给出
//          len:待发送数据的字节数
//          buff:待发送数据发送缓冲区首地址
//功能概要:CAN模块发送一次数据
//=====================================================================
uint8_t can_send_once(uint8_t canNo, uint32_t DestID, uint16_t len ,uint8_t* buff)
{
	//(1)定义Can发送函数所需要用到的变量
	uint32_t transmit_mailbox;
	uint32_t register_tsr;
	uint32_t rtr;
	rtr = CAN_RTR_DATA;
	register_tsr = READ_REG(CAN_ARR[canNo-1]->TSR);

	//(2)判断3个邮箱中是否有空闲邮箱,若有,选取其中一个进行发送,选取顺序为1,2,3
    if (((register_tsr & CAN_TSR_TME0) != 0U) ||    
        ((register_tsr & CAN_TSR_TME1) != 0U) ||
        ((register_tsr & CAN_TSR_TME2) != 0U))
    {
        //register_tsr是从CAN的发送状态寄存器中读取的值,CAN_TSR_CODE掩码表示发送邮箱的编码位置,CAN_TSR_CODE_Pos表示编码位置偏移
    	transmit_mailbox = (register_tsr & CAN_TSR_CODE) >> CAN_TSR_CODE_Pos;     //读取空闲邮箱编码
    	if(transmit_mailbox > 2U)
    	{
    		return 1;
    	}

    	//(2.1)判断并设置发送帧为标准帧还是扩展帧
        //根据 DestID的值来确定帧类型
    	if(DestID <= 0x7FFU)    //标准帧使用11位标识符,范围是0x000到0x7FF
    	{
    		CAN_ARR[canNo-1]->sTxMailBox[transmit_mailbox].TIR = ((DestID << CAN_TI0R_STID_Pos)|CAN_ID_STD|rtr);  //设置标准帧标识符
    	}
    	else //扩展帧使用 29 位标识符,范围是0x00000000到0x1FFFFFFF
    	{
    		CAN_ARR[canNo-1]->sTxMailBox[transmit_mailbox].TIR = ((DestID << CAN_TI0R_EXID_Pos)|CAN_ID_EXT|rtr);  //设置扩展帧标识符
    	}
    	//(2.2)设置发送帧的数据长度
    	CAN_ARR[canNo-1]->sTxMailBox[transmit_mailbox].TDTR = len;
        //SET_BIT(CAN_ARR[canNo-1]->sTxMailBox[transmit_mailbox].TDTR, CAN_TDT0R_TGT);
        //(2.3)设置发送帧的数据
        //TDHR和TDLR是CAN发送邮箱的两个数据寄存器,用于存储待发送的数据帧的高4个字节和低4个字节
        //每个寄存器都有4个数据字段,每个字段对应一个字节的数据
        WRITE_REG(CAN_ARR[canNo-1]->sTxMailBox[transmit_mailbox].TDHR,   //依次将每个字节的数据左移到相应的位置,然后写入到TDHR寄存器中
                  ((uint32_t)buff[7] << CAN_TDH0R_DATA7_Pos) |
                  ((uint32_t)buff[6] << CAN_TDH0R_DATA6_Pos) |
                  ((uint32_t)buff[5] << CAN_TDH0R_DATA5_Pos) |
                  ((uint32_t)buff[4] << CAN_TDH0R_DATA4_Pos));
        WRITE_REG(CAN_ARR[canNo-1]->sTxMailBox[transmit_mailbox].TDLR,  //依次将每个字节的数据左移到相应的位置,然后写入到TDLR寄存器中
                  ((uint32_t)buff[3] << CAN_TDL0R_DATA3_Pos) |
                  ((uint32_t)buff[2] << CAN_TDL0R_DATA2_Pos) |
                  ((uint32_t)buff[1] << CAN_TDL0R_DATA1_Pos) |
                  ((uint32_t)buff[0] << CAN_TDL0R_DATA0_Pos));
        //(2.4)发送Can数据报
        SET_BIT(CAN_ARR[canNo-1]->sTxMailBox[transmit_mailbox].TIR, CAN_TI0R_TXRQ);   //设置指定发送邮箱的TIR寄存器中的TXRQ位来触发CAN控制器发送已经准备好的数据帧
        return 0;
    }
    else
    {
    	return 1;
    }
}

代码实现了 CAN 数据帧的一次发送。首先检查是否有空闲的发送邮箱,如果有,选择其中一个发送数据。根据DestID设置为标准帧或扩展帧,然后设置数据长度和数据内容,最后发送数据帧。

//=====================================================================
//函数名称:CAN_HWInit
//函数返回:0=正常,1=错误
//参数说明:CANChannel:硬件引脚组号,共有3组,分别为PTA11&PTA12(CAN_CHANNEL0),PTB8&PTB9(CAN_CHANNEL1),PTD0&PTD1(2)
//功能概要:CAN模块引脚初始化
//=====================================================================
uint8_t CAN_HWInit(uint8_t CANChannel)
{
	if(CANChannel < 0 || CANChannel > 2)
	{
		return 1;
	}
	if(CANChannel == 0)
	{
		RCC->APB1ENR1 |= RCC_APB1ENR1_CAN1EN;   //使能CAN1模块的时钟
		RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
		GPIOA->MODER &= ~(GPIO_MODER_MODE11|GPIO_MODER_MODE12);
		GPIOA->MODER |= (GPIO_MODER_MODE11_1|GPIO_MODER_MODE12_1);  //将GPIOA的引脚11和12设置为复用功能模式
		GPIOA->AFR[1] &= ~(GPIO_AFRH_AFSEL11|GPIO_AFRH_AFSEL12);
		GPIOA->AFR[1] |= (GPIO_AFRH_AFSEL11_0|GPIO_AFRH_AFSEL11_3)|(GPIO_AFRH_AFSEL12_0|GPIO_AFRH_AFSEL12_3);   //设置GPIOA的引脚11和12的复用功能为CAN1
	}
    //下面的逻辑一样的,不带注释了
	else if(CANChannel == 1)
	{
		RCC->APB1ENR1 |= RCC_APB1ENR1_CAN1EN;
		RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
		GPIOB->MODER &= ~(GPIO_MODER_MODE8|GPIO_MODER_MODE9);
		GPIOB->MODER |= (GPIO_MODER_MODE8_1|GPIO_MODER_MODE9_1);
		GPIOB->AFR[1] &= ~(GPIO_AFRH_AFSEL8|GPIO_AFRH_AFSEL9);
		GPIOB->AFR[1] |= ((GPIO_AFRH_AFSEL8_0|GPIO_AFRH_AFSEL8_3)|
						  (GPIO_AFRH_AFSEL9_0|GPIO_AFRH_AFSEL9_3));
	}
	else
	{
		RCC->APB1ENR1 |= RCC_APB1ENR1_CAN1EN;
		RCC->AHB2ENR |= RCC_AHB2ENR_GPIODEN;
		GPIOD->MODER &= ~(GPIO_MODER_MODE0|GPIO_MODER_MODE1);
		GPIOD->MODER |= (GPIO_MODER_MODE0_1|GPIO_MODER_MODE1_1);
		GPIOD->AFR[0] &= ~(GPIO_AFRL_AFSEL0|GPIO_AFRL_AFSEL1);
		GPIOD->AFR[0] |= ((GPIO_AFRL_AFSEL0_0 | GPIO_AFRL_AFSEL0_3)|
						  (GPIO_AFRL_AFSEL1_0 | GPIO_AFRL_AFSEL1_3));
	}
	return 0;
}

根据传入的参数,初始化对应的CAN模块和GPIO引脚,先判断参数是否有效,然后初始化对应的CAN模块和GPIO引脚,包括使能时钟、配置引脚模式和复用功能。

//=====================================================================
//函数名称:CAN_SWInit_Entry
//函数返回:0=正常,1=错误
//参数说明:canNo:模块基地址号,本芯片只有CAN_1,
//功能概要:进入初始化模式
//=====================================================================
uint8_t CAN_SWInit_Entry(uint8_t canNo)
{
	int i;
	CLEAR_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_SLEEP);    // 清除睡眠模式位,确保CAN控制器不是在睡眠模式下
	i = 0;
	while ((CAN_ARR[canNo-1]->MSR & CAN_MSR_SLAK) != 0U)   // 一直等,直到CAN控制器退出睡眠模式
	{
		if(i++ > 0x30000)
		{
			return 1;
		}
	}

	SET_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_INRQ);    // 设置初始化请求位,进入初始化模式
	i = 0;
	while ((CAN_ARR[canNo-1]->MSR & CAN_MSR_INAK) == 0U)// 一直等,直到CAN控制器进入初始化模式
	{
		if(i++ > 0x30000)
		{
			return 1;
		}
	}
	return 0;
}

将指定的CAN控制器置于初始化模式。

//=====================================================================
//函数名称:CAN_SWInit_CTLMode
//函数返回:无
//参数说明:canNo:模块基地址号,本芯片只有CAN_1,
//功能概要:CAN总线模式设置
//=====================================================================
void CAN_SWInit_CTLMode(uint8_t canNo)
{
	CLEAR_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_TTCM);   //清除了MCR中的TTCM位,禁用了时间触发通信模式
	CLEAR_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_ABOM);   //清除了MCR中的ABOM位,禁用了自动离线管理功能
	CLEAR_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_AWUM);   //清除了MCR中的AWUM位,禁用了自动唤醒模式
	SET_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_NART);     //设置了MCR中的NART位,启用了非自动重传模式,即在发送失败时不自动重传消息
	CLEAR_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_RFLM);    //清除了MCR中的RFLM位,禁用了接收FIFO锁定模式
	CLEAR_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_TXFP);    //清除了MCR中的TXFP位,使发送FIFO按照先到先发送的原则处理消息
}

函数通过设置和清除CAN控制器模式寄存器(MCR)中的特定位,配置了CAN总线的工作模式。这些配置包括禁用时间触发通信模式、自动离线管理和自动唤醒模式,启用非自动重传模式,并禁用接收FIFO锁定模式和发送FIFO优先级。

//函数名称:CAN_SWInit_CTLMode
//函数返回:无
//参数说明:canNo:模块基地址号,本芯片只有CAN_1,
//			CANMode:CAN总线工作模式,分别为正常模式(CAN_MODE_NORMAL)、回环模式(CAN_MODE_LOOPBACK)、
//										    静默模式(CAN_MODE_SILENT)以及回环与静默组合模式(CAN_MODE_SILENT_LOOPBACK)
//功能概要:CAN总线位时序配置
void CAN_SWInit_BT(uint8_t canNo, uint32_t CANMode, uint32_t Prescaler)
{
    //位时序配置包括设置同步跳转宽度(SJW)、时间段1(TS1)和时间段2(TS2)。工作模式包括正常模式、回环模式、静默模式以及回环与静默组合模式
	CAN_ARR[canNo-1]->BTR |= ((uint32_t)(Prescaler-1)|CAN_SJW_1TQ|CAN_BTR_TS1_1|CAN_BTR_TS1_0|CAN_BTR_TS2_2|CANMode);    //配置CAN控制器的位时序和工作模式
}

这段代码用于配置CAN控制器的位时序和工作模式

//=====================================================================
//函数名称:CAN_SWInit_Quit
//函数返回:0=正常,1=错误
//参数说明:canNo:模块基地址号
//功能概要:退出初始化模式,进入正常模式
//=====================================================================
uint8_t CAN_SWInit_Quit(uint8_t canNo)
{
	int i;
	CLEAR_BIT(CAN_ARR[canNo-1]->MCR, CAN_MCR_INRQ);    //清除CAN控制器的模式控制寄存器中的初始化请求位INRQ
	i = 0;
    while ((CAN_ARR[canNo-1]->MSR & CAN_MSR_INAK) != 0U)    //持续检查CAN控制器的模式状态寄存器中的初始化确认位INAK
    {
      if (i++ > 0x30000)
      {
        return 1;
      }
    }
    return 0;
}

这个函数通过清除CAN控制器的初始化请求位,使CAN控制器退出初始化模式,并等待初始化确认位被清除以确认成功退出。

//=====================================================================
// 函数名称: CANFilterConfig
// 函数返回:0=正常,1=错误
// 参数说明: canNo:模块基地址号,
//		    canID:自身CAN节点的唯一标识,例如按照CANopen协议给出
//		    Can_Rx_FifoNo:中断使用的邮箱号,
//			IsActivate:是否激活过滤器
//			CANFilterBank:CAN总线过滤器组选择,共有28个,(CANFilterBank0~CANFilterBank27)
//			CANFiltermode:CAN总线过滤器模式,分别为掩码模式(CAN_FILTERMODE_IDMASK)和列表模式(CAN_FILTERMODE_IDLIST)
//			CAN_Filterscale:CAN总线过滤器位数,分别为32位(CAN_FILTERSCALE_32BIT)和16位(CAN_FILTERSCALE_16BIT)
// 功能概要:CAN接收中断开启
//=====================================================================
uint8_t CANFilterConfig(uint8_t canNo, uint32_t CanID, uint32_t FilterBank, uint32_t Can_Rx_FifoNo, uint8_t IsActivate, uint32_t FilterMode, uint32_t FilterScale)
{
	uint32_t FilterIdHigh, FilterIdLow, FilterMaskIdHigh, FilterMaskIdLow, filternbrbitpos;
	if (CanID <= 0x7FFU)
		CanID = CanID << CAN_TI0R_STID_Pos; // 如果CanID小于等于0x7FF(即标准 11 位标识符),则将CanID左移指定的位数,使其符合CAN标识符的格式

	FilterIdHigh = (CanID >> 16) & 0xFFFF;				   // 得到高16位CanID,这个作为过滤器的ID
	FilterIdLow = (CanID & 0xFFFF);						   // 得到低16位CanID,这个作为过滤器的ID
	FilterMaskIdHigh = 0xFFE0;							   // 定义过滤器掩码的高16位
	FilterMaskIdLow = 0x0000;							   // 定义过滤器掩码的低16位
	filternbrbitpos = (uint32_t)1 << (FilterBank & 0x1FU); // 计算过滤器位位置

	// 设置过滤器初始化模式 (FINIT=1),在此模式下可以进行过滤器初始化
	SET_BIT(CAN_ARR[canNo - 1]->FMR, CAN_FMR_FINIT);	  // 设置CAN过滤器的初始化模式(FINIT),允许配置过滤器
	CLEAR_BIT(CAN_ARR[canNo - 1]->FA1R, filternbrbitpos); // 清除与指定过滤器组关联的激活位,以禁用该过滤器
	if (FilterScale == CAN_FILTERSCALE_16BIT)			  // 如果使用16位过滤器,
	{
		CLEAR_BIT(CAN_ARR[canNo - 1]->FS1R, filternbrbitpos); // 清除位位置,以配置为16位模式
		CAN_ARR[canNo - 1]->sFilterRegister[FilterBank].FR1 =
			((0x0000FFFFU & (uint32_t)FilterMaskIdLow) << 16U) |
			(0x0000FFFFU & (uint32_t)FilterIdLow);
		CAN_ARR[canNo - 1]->sFilterRegister[FilterBank].FR2 =
			((0x0000FFFFU & (uint32_t)FilterMaskIdHigh) << 16U) |
			(0x0000FFFFU & (uint32_t)FilterIdHigh);
	}
	if (FilterScale == CAN_FILTERSCALE_32BIT) // 如果使用32位过滤器
	{
		SET_BIT(CAN_ARR[canNo - 1]->FS1R, filternbrbitpos);	  // 设置位位置,以配置为32位模式
		CAN_ARR[canNo - 1]->sFilterRegister[FilterBank].FR1 = // 设置过滤器寄存器1和2
			((0x0000FFFFU & (uint32_t)FilterIdHigh) << 16U) |
			(0x0000FFFFU & (uint32_t)FilterIdLow);
		CAN_ARR[canNo - 1]->sFilterRegister[FilterBank].FR2 =
			((0x0000FFFFU & (uint32_t)FilterMaskIdHigh) << 16U) |
			(0x0000FFFFU & (uint32_t)FilterMaskIdLow);
	}
	if (FilterMode == CAN_FILTERMODE_IDMASK) // 如果使用掩码模式
	{
		CLEAR_BIT(CAN_ARR[canNo - 1]->FM1R, filternbrbitpos); // 清除位位置,以选择掩码模式
	}
	else
	{
		SET_BIT(CAN_ARR[canNo - 1]->FM1R, filternbrbitpos);
	}
	if (Can_Rx_FifoNo == CAN_FILTER_FIFO0) // 设置过滤器FIFO
	{
		CLEAR_BIT(CAN_ARR[canNo - 1]->FFA1R, filternbrbitpos); // 清除FIFO分配寄存器位
	}
	else
	{
		SET_BIT(CAN_ARR[canNo - 1]->FFA1R, filternbrbitpos);
	}
	if (IsActivate == 1) // 如果为1,则激活过滤器
	{
		SET_BIT(CAN_ARR[canNo - 1]->FA1R, filternbrbitpos); // 设置过滤器激活寄存器位
	}
	// 退出过滤器初始化模式 (FINIT=0)
	CLEAR_BIT(CAN_ARR[canNo - 1]->FMR, CAN_FMR_FINIT); // 清除CAN过滤器初始化模式

	return 0;
}

这个函数用于配置CAN总线的过滤器,设置过滤器ID、掩码、模式、FIFO和激活状态。这个函数确保了CAN控制器能够正确地过滤接收到的CAN消息。

OK,也是终于把这段代码分析完毕了,那么第十章的学习内容大概就是这这些了,我只能说,对于这个CAN,要是想深入学习,还是挺多东西要学的,我们解析代码也是充其量要知道这个函数怎么用,大概知道CAN通信的原理和步骤。

作业(即实验内容)

2个或以上同学相互连接,利用CAN通信,向对方发送带有本人姓名的信息。连线方式:按基本原理性电路(不带收发器芯片)连接,参考教材图10-1。

这里略,只要用示例的程序,连接好开发板,就可以进行通信了。

在ADC实验中,结合热敏电阻,分别通过触摸芯片表面和热敏电阻,引起A/D值变化,显示芯片内部温度和当前温度。

这个简单,仅仅我们已经有AD值了,根据AD值用公式转换成温度就好了,以下是main.c的代码

#define GLOBLE_VAR
#include "includes.h"      //包含总头文件

void Delay_ms(uint16_t u16ms);
float Regression_Ext_Temp(uint16_t tmpAD);      //环境温度AD值转为实际温度
float adc_mcu_tempmain(uint16_t mcu_temp_AD);   //MCU温度AD值转为实际温度

int main(void)
{
    // 声明main函数使用的局部变量
    uint16_t num_AD2;
    uint16_t num_AD3;
    
    // 关总中断
    DISABLE_INTERRUPTS;

    // 用户外设模块初始化
    adc_init(ADC_CHANNEL_15,AD_DIFF);			    //初始化ADC通道15,差分mode
    adc_init(ADC_CHANNEL_TEMPSENSOR,AD_SINGLE);	//初始化ADC通道:内部温度,单端mode
    
    emuart_init(UART_User,115200);
    // 使能模块中断
    uart_enable_re_int(UART_User);
    
    // 开总中断
    ENABLE_INTERRUPTS;
    
    // 主循环
    for(;;)
    {
		Delay_ms(2000);	// 延时2s
        num_AD2 = adc_ave(ADC_CHANNEL_15,8);
        num_AD3 = adc_ave(ADC_CHANNEL_TEMPSENSOR,8);
        printf("wyw wyw wyw wyw wyw wyw wyw wyw wyw\r\n");
        printf("通道15(GEC12、11)的A/D值:%d\r\n",num_AD2);
        printf("通道15(GEC12、11)的温度值(即环境温度):%f\r\n",Regression_Ext_Temp(num_AD2));
        printf("内部温度传感器的A/D值:%d\r\n",num_AD3);
        printf("内部温度传感器的温度值(即芯片内部温度):%f\r\n",adc_mcu_tempmain(num_AD3));
        printf("wyw wyw wyw wyw wyw wyw wyw wyw wyw\r\n");
    }
}

//======以下为主函数调用的子函数===========================================
//======================================================================
//函数名称:Delay_ms
//函数返回:无
//参数说明:无
//功能概要:延时 - 毫秒级
//======================================================================
void Delay_ms(uint16_t u16ms)
{
    uint32_t u32ctr;
    for(u32ctr = 0; u32ctr < 8000*u16ms; u32ctr++)
    {
        __ASM("NOP");
    }
}

//============================================================================
//函数名称:Regression_Ext_Temp
//功能概要:将读到的环境温度AD值转换为实际温度
//参数说明:tmpAD:通过adc_read函数得到的AD值
//函数返回:实际温度值
//============================================================================
float Regression_Ext_Temp(uint16_t tmpAD)
{
    float Vtemp,Rtemp,temp;
    if(tmpAD<=72)
    {
       return -274;
    }
    Vtemp = (tmpAD*3300.0)/4096;
    Rtemp = Vtemp/(3300.0 - Vtemp)*10000.0;
    temp = (1/(log(Rtemp/10000.0)/3950.0 + (1/(273.15 + 25)))) - 273.15 + 0.5; 
    return temp; 
}

//============================================================================
//函数名称:adc_mcu_tempmain
//功能概要:将读到的芯片内部mcu温度AD值转换为实际温度
//参数说明:mcu_temp_AD:通过adc_read函数得到的AD值
//函数返回:实际温度值
//============================================================================
float adc_mcu_tempmain(uint16_t mcu_temp_AD)
{
	float mcu_temp_result;
	mcu_temp_result=(float)(6+55+(100*((float)(mcu_temp_AD) - AD_CAL1))/(AD_CAL2 - AD_CAL1));
	return mcu_temp_result;
 
}

接下来我们运行一下,看看结果,某一时刻可以检测到如下结果

我们可以看到,对于通道15,测得的AD值为2490,由于是热敏电阻,我们能根据电阻的电压来判断温度,温度为15.947104,对于内部传感器,AD值为885,对应的温度为15.354354,OK,某一时刻,我们用我们的手指按住热敏电阻,结果如下

可以看到,一下子通道15的AD值就降下去很多了,同时可以观察到外部温度变成38.9度了,这证明了我是多么热情似火(希望不是发烧哈哈哈),接下来再来测试一下芯片内部的温度,用手按住芯片表面,一段时间过后,结果如下

我只能说由于我们触摸的是芯片的外部,要影响到内部这个热量导不进去,捂了半天也才上了5度,可以观察到,AD值由原来的885上升到了901,温度也大致上升了5度。那么这里就到这里为止了

用实验验证,对于有数据的某扇区,如果没有擦除(Flash_erase),可否写入新数据?注:扇区号为学号后2位,数据文本中要有姓名。

这个也简单,我们在示例程序那里稍微改一改验证一下就知道了,以下是main.c

#define GLOBLE_VAR
#include "includes.h"      //包含总头文件


int main(void)
{
	// 定义变量
	uint8_t mK1[32];	  //按照逻辑读方式从指定flash区域中读取的数据
	uint8_t result;    	  //判断扇区是否为空标识
	
	// 关总中断
	DISABLE_INTERRUPTS;

	// Flash初始化
	flash_init();
   
	// 开总中断
	ENABLE_INTERRUPTS;
    
    // 擦除第71扇区
	flash_erase(71);   
	
	result = flash_isempty(71,MCU_SECTORSIZE); // 判断第71扇区是否为空
	printf("wyw wyw wyw wyw wyw wyw wyw wyw wyw wyw\n");
	printf("第71扇区是否为空,1表示空,0表示不空: %d\n",result);
	
	// 第一次写入
	flash_write(71,0,32,(uint8_t *) "I am Wang Yuewei. I am handsome.");
	flash_read_logic(mK1,71,0,32); //从71扇区读取32个字节到mK1中
	printf("第一次: %s\n",mK1);
	result = flash_isempty(71,MCU_SECTORSIZE); // 判断第71扇区是否为空
	printf("第71扇区是否为空,1表示空,0表示不空: %d\n",result);
	
	// 第二次写入
	flash_write(71,0,32,(uint8_t *) "I am Wang Weiyue! I am handsome!");
	flash_read_logic(mK1,71,0,32); //从71扇区读取32个字节到mK1中
	printf("第二次: %s\n",mK1);
	printf("wyw wyw wyw wyw wyw wyw wyw wyw wyw wyw\n");
	
	// 请在第一次运行之后找到flash.c的函数flash_write,里面有一个擦除扇区操作,注释掉在编译运行一次
	// 看看结果如何
}

如上面的代码,我们使用逻辑写的方式进行写入,物理写一样的,因此就不看了,我们在第一次第二次写入的之间,不使用擦除函数进行擦除,看看效果,如下

我们观察到,这似乎是覆盖写入成功了?但我只能说,事实上真的是这样吗?对于同一片扇区的同样的为止,不擦除真的可以写入数据吗?别急,打开我们的flash.c看看写函数。如下

上图为write函数的部分代码,可以看到,实际上函数内部就有做一个擦除当前操作扇区的操作,我们现在将他注释掉,再编译运行一下看看,结果如下

没错,结果如我们所料,第二次的数据没有写入成功,还是第一次的数据,所以实际上对于有数据的某扇区,如果没有擦除(Flash_erase),是不可以写入新数据的

最后的总结

不知不觉间,本学期对于嵌入式的学习也接近了尾声了,在整个学习的过程中,还是收获了很多的,从第一个嵌入式程序开始,我们逐渐学习,从一开始的汇编回忆杀,到普通的点灯,再到花式的点灯,再到串口通信,定时器模块以及我们这次作业的FLASH,ADC/DAC,CAN,我逐渐了解了这些知识,在这里我也不总结我这次的博客的内容了,最后,请以我这一系列的博客,在互联网上留下我曾学习过的记忆吧,到此,嵌入式系列学习博客,收官!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值