GD32F450z评估板的开发 (2)

 

本文记录GD32F450z的开发全过程

本文记录GD32F450z的开发全过程

1. SPI FLASH

1.1 简介

串行外设接口(Serial Peripheral Interface,缩写为 SPI)提供了基于 SPI 协议的数据发送和接收功能,可以工作于主机或从机模式。SPI5 还支持 SPI 四线主机模式。

1.1.1 SPI 结构框图

spi

1.2 源码分析

1.2.1 初始化SPI5 GPIO

1.2.1.1 启用GPIOG和SPI5时钟

  • 启用设备时钟
      rcu_periph_clock_enable(RCU_GPIOG);
      rcu_periph_clock_enable(RCU_SPI5);
    
      /*!< SPI5 clock */
      RCU_SPI5 = RCU_REGIDX_BIT(APB2EN_REG_OFFSET, 21U)
    

1.2.1.2 设置GPIOG引脚

  1. 配置GPIOG引脚模式
     /* SPI5_CLK(PG13), SPI5_MISO(PG12), 
     SPI5_MOSI(PG14), SPI5_IO2(PG10) and 
     SPI5_IO3(PG11) GPIO pin configuration */
     // GPIOG引脚设置为备用功能5
     gpio_af_set(GPIOG, GPIO_AF_5, 
       GPIO_PIN_10|GPIO_PIN_11| GPIO_PIN_12|GPIO_PIN_13| GPIO_PIN_14);
     // 引脚电阻浮空
     gpio_mode_set(GPIOG, GPIO_MODE_AF, GPIO_PUPD_NONE, 
       GPIO_PIN_10|GPIO_PIN_11| GPIO_PIN_12|GPIO_PIN_13| GPIO_PIN_14);
     // GPIO[SPI5]引脚为 push pull 模式
     gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ,
       GPIO_PIN_10|GPIO_PIN_11| GPIO_PIN_12|GPIO_PIN_13| GPIO_PIN_14);
    
     /* SPI5_CS(PG9) GPIO pin configuration */
     // GPIOG[9]为输出模式,浮空模式
     gpio_mode_set(GPIOG, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_9);
     // GPIOG[9]为 push pull 模式
     gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
    
  2. Flash片选信号无效

    spi_flash

    注意 评估板上JP10处的跳线要短接到SPI端,否则读取Flash ID会失败(返回0xFF)

     #define  SPI_FLASH_CS_HIGH()       gpio_bit_set(GPIOG,GPIO_PIN_9)
    
  3. 配置SPI5参数
     // SPI 传输模式:SPI在全双工通信中接收和发送数据 
     spi_init_struct.trans_mode           = SPI_TRANSMODE_FULLDUPLEX;
     //SPI5 主机 BIT(2)|BIT(8)
     spi_init_struct.device_mode          = SPI_MASTER;
     // SPI数据帧为8bit
     spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;
     // SPI时钟极性为低电平,相位为第一边缘 
     spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
     // NSS 软件模式选择:由软件控制,BIT(9)
     spi_init_struct.nss                  = SPI_NSS_SOFT;
     // SPI时钟预分步系数为32 
     spi_init_struct.prescale             = SPI_PSC_32;
     // SPI的传输方式是big endian:先传输MSB 
     spi_init_struct.endian               = SPI_ENDIAN_MSB;
     spi_init(SPI5, &spi_init_struct);
    
     SPI_I2SCTL(spi_periph) &= (uint32_t)(~SPI_I2SCTL_I2SSEL);
    

    I2S 控制寄存器 (SPI_I2SCTL)

    地址偏移:0x1C

    名称 说明
    11 I2SSEL I2S 模式选择
    0:SPI 模式
    1:I2S 模式
    当 SPI 模式或 I2S 模式关闭时配置该位
  4. 设置CRC多项式
     spi_crc_polynomial_set(SPI5,7) {
       /* enable SPI CRC */
       SPI_CTL0(spi_periph) |= (uint32_t)SPI_CTL0_CRCEN;
    
       /* set SPI CRC polynomial */
       SPI_CRCPOLY(spi_periph) = (uint32_t)crc_poly;
     }
    

    控制寄存器 0 (SPI_CTL0)

    地址偏移:0x00

    名称 说明
    13 CRCEN CRC 计算使能
    0:CRC 计算禁止
    1:CRC 计算使能

    CRC 多项式寄存器 (SPI_CRCPOLY)

    地址偏移:0x10 复位值:0x0007

    名称 说明
    15:0 CPR[15:0] CRC 多项式寄存器值
    该值包含了 CRC 多项式,用于 CRC 计算,默认值为 0007h
  5. 四线SPI_IO2和SPI_IO3引脚输出使能
     qspi_io23_output_enable(SPI5) {
       SPI_QCTL(spi_periph) |= (uint32_t)SPI_QCTL_IO23_DRV;
     }
    

    SPI5 四路 SPI 控制寄存器 (SPI_QCTL)

    地址偏移:0x80

    名称 说明
    2 IO23_DRV IO2 和 IO3 输出使能
    0:单路模式下 IO2 和 IO3 输出关闭
    1:单路模式下 IO2 和 IO3 输出高电平
    该位仅适用于 SPI5
  6. 使能SPI
     spi_enable(SPI5);
    

1.2.2 获取芯片信息

1.2.2.1 获取CPU序列号

void get_chip_serial_num(void) {
    int_device_serial[0] = *(__IO uint32_t*)(0x1FFF7A10);
    int_device_serial[1] = *(__IO uint32_t*)(0x1FFF7A14);
    int_device_serial[2] = *(__IO uint32_t*)(0x1FFF7A18);
}

设备唯一 ID (96 位/位域)

基地址:0x1FFF_7A10 ~ 0x1FFF_7A18

该值是原厂设定的,不能由用户修改

地址 名称 说明
0x1FFF 7A10 UNIQUE_ID[31:0] 设备唯一 ID
0x1FFF 7A14 UNIQUE_ID[63:32] 设备唯一 ID
0x1FFF 7A18 UNIQUE_ID[95:64] 设备唯一设备 ID

1.2.2.2 获取Flash ID

  1. 选择FLash芯片
     SPI_FLASH_CS_LOW();
    
  2. 发送RDID指令
     spi_flash_send_byte(RDID);
    
     #define RDID  0x9F     /* read identification */
    

    其中:

     // 通过SPI接口发送一个字节,并返回从SPI总线收到的字节
     uint8_t spi_flash_send_byte(uint8_t byte)
     {
         /* loop while data register in not empty */
         // TBE: 发送缓冲区空
         while(RESET == spi_i2s_flag_get(SPI5,SPI_FLAG_TBE));
    
         /* send byte through the SPI5 peripheral */
         /* SPI_DATA(spi_periph) = (uint32_t)data; */
         spi_i2s_data_transmit(SPI5,byte);
    
         /* wait to receive a byte */
         // RBNE: 接收缓冲区非空
         while(RESET == spi_i2s_flag_get(SPI5,SPI_FLAG_RBNE));
    
         /* return the byte read from the SPI bus */
         // ((uint16_t)SPI_DATA(spi_periph));
         return(spi_i2s_data_receive(SPI5));
     }
    
  3. 从Flash中读取一个字节
     // 发送伪码dummy byte,为的是产生时钟和CS有效
     temp0 = spi_flash_send_byte(DUMMY_BYTE);
     temp1 = spi_flash_send_byte(DUMMY_BYTE);
     temp2 = spi_flash_send_byte(DUMMY_BYTE);
    
     #define DUMMY_BYTE       0xA5
    
  4. 取消选择闪存:芯片选择高电平
     SPI_FLASH_CS_HIGH();
    
  5. 读取到的内容:
     temp = (temp0 << 16) | (temp1 << 8) | temp2;
    

    1.2.3 向flash写数据

判断Flash芯片型号合法

if(SFLASH_ID == flash_id)
#define SFLASH_ID                0xC84015

擦除指定的闪存扇区

spi_flash_sector_erase(FLASH_WRITE_ADDRESS);

#define FLASH_WRITE_ADDRESS      0x000000
  • 开启对flash的写访问
      void spi_flash_write_enable(void) {
          /* select the flash: chip select low */
          SPI_FLASH_CS_LOW();
    
          /* send "write enable" instruction */
          // #define WREN   0x06     /* write enable instruction */
          spi_flash_send_byte(WREN);
    
          /* deselect the flash: chip select high */
          SPI_FLASH_CS_HIGH();
      }
    
  • 擦除扇区
    SPI_FLASH_CS_LOW();
    // 发送擦除指令send sector erase instruction
    spi_flash_send_byte(SE);
    // 发送扇区地址的字节
    spi_flash_send_byte((sector_addr & 0xFF0000) >> 16);
    spi_flash_send_byte((sector_addr & 0xFF00) >> 8);
    spi_flash_send_byte(sector_addr & 0xFF);
    
    SPI_FLASH_CS_HIGH();
    
    // 等到Flash写完
    spi_flash_wait_for_write_end();
    

    其中:

    spi_flash_wait_for_write_end() {
      SPI_FLASH_CS_LOW();
    
      /* send "read status register" instruction */
      spi_flash_send_byte(RDSR);
    
      do{
          /* send a dummy byte to generate the clock needed by the flash
          and put the value of the status register in flash_status variable */
          flash_status = spi_flash_send_byte(DUMMY_BYTE);
      }while(SET == (flash_status & WIP_FLAG));
    }
    // #define WIP_FLAG   0x01     /* write in progress(wip) flag */
    

向Flash写数据

qspi_flash_buffer_write(tx_buffer,FLASH_WRITE_ADDRESS,256);

#define FLASH_WRITE_ADDRESS      0x000000
  1. 对要写入的地址分页
     #define  SPI_FLASH_PAGE_SIZE       0x100
    
     addr = write_addr % SPI_FLASH_PAGE_SIZE;
     count = SPI_FLASH_PAGE_SIZE - addr;
     num_of_page =  num_byte_to_write / SPI_FLASH_PAGE_SIZE;
     num_of_single = num_byte_to_write % SPI_FLASH_PAGE_SIZE;
    
  2. 如果要写入的地址是页对齐的
     if(addr == 0){
       // 写入长度在页内
         if(num_of_page == 0){
             qspi_flash_page_write(pbuffer, write_addr, 
                                   num_byte_to_write);
         }else{            
           while(num_of_page--){
               qspi_flash_page_write(pbuffer, write_addr, 
                                 SPI_FLASH_PAGE_SIZE);
               write_addr +=  SPI_FLASH_PAGE_SIZE;
               pbuffer += SPI_FLASH_PAGE_SIZE;
           }
           qspi_flash_page_write(pbuffer, write_addr, num_of_single);
         }
    

    其中:

     // 使用qspi向flash写入一个以上的字节
     void qspi_flash_page_write(
           uint8_t* pbuffer, 
           uint32_t write_addr, 
           uint16_t num_byte_to_write) {
    
         /* enable the flash quad mode */
         qspi_flash_quad_enable();
         spi_flash_write_enable();
         SPI_FLASH_CS_LOW();
    
         /* send "quad write to memory " instruction */
         spi_flash_send_byte(QUADWRITE);
    
         // 发送扇区地址的字节
         spi_flash_send_byte((write_addr & 0xFF0000) >> 16);
         spi_flash_send_byte((write_addr & 0xFF00) >> 8);
         spi_flash_send_byte(write_addr & 0xFF);
    
         /* enable the qspi */ 
         qspi_enable(SPI5); 
         // 启用qspi写操作
         qspi_write_enable(SPI5);
    
         /* while there is data to be written on the flash */
         while(num_byte_to_write--){
             spi_flash_send_byte(*pbuffer++);
         }
    
         /* deselect the flash: chip select high */
         SPI_FLASH_CS_HIGH();
         /* disable the qspi function */ 
         qspi_disable(SPI5); 
         /* wait the end of flash writing */
         spi_flash_wait_for_write_end();
     }
    

    其中:

     // 开始flash的quad四线模式
     void qspi_flash_quad_enable(void)
     {
       /* enable the write access to the flash */
       spi_flash_write_enable();
    
       SPI_FLASH_CS_LOW();
    
       /* send "write status register" instruction */
       spi_flash_send_byte(WRSR);
       spi_flash_send_byte(0x00);
       spi_flash_send_byte(0x02); 
    
       SPI_FLASH_CS_HIGH(); 
       spi_flash_wait_for_write_end();
     }
    
    • Qual SPI   
      • 针对SPI Flash,Qual SPI Flash增加了两根I/O线(SIO2,SIO3),目的是一个时钟内传输4个bit
     // 启用四线SPI  
     void qspi_enable(uint32_t spi_periph) {
         SPI_QCTL(spi_periph) |= (uint32_t)SPI_QCTL_QMOD;
     }
    

    SPI5 四路 SPI 控制寄存器 (SPI_QCTL)

    地址偏移:0x80

    名称 说明
    0 QMOD 四路 SPI 模式使能
    0:SPI 工作在单路模式
    1:SPI 工作在四路模式
    该位仅能在 SPI 未通信时配置(TRANS 位清零)
  3. 如果写入的地址不是对齐的,方法也上同,只是地址的处理上略有差别

从Flash读取数据

qspi_flash_buffer_read(rx_buffer,FLASH_READ_ADDRESS,256)
{
    SPI_FLASH_CS_LOW();

    /* send "quad fast read from memory " instruction */
    spi_flash_send_byte(QUADREAD);

    spi_flash_send_byte((read_addr & 0xFF0000) >> 16);
    spi_flash_send_byte((read_addr & 0xFF00) >> 8);
    spi_flash_send_byte(read_addr & 0xFF);

    /* enable the qspi */
    qspi_enable(SPI5); 
    // SPI_QCTL(spi_periph) |= (uint32_t)SPI_QCTL_QMOD;
    /* enable the qspi read operation */
    qspi_read_enable(SPI5);
    // SPI_QCTL(spi_periph) |= (uint32_t)SPI_QCTL_QRD;

    spi_flash_send_byte(0xA5);
    spi_flash_send_byte(0xA5);
    spi_flash_send_byte(0xA5);
    spi_flash_send_byte(0xA5);

    /* while there is data to be read */
    while(num_byte_to_read--){
        /* read a byte from the flash */
        *pbuffer = spi_flash_send_byte(DUMMY_BYTE);
        /* point to the next location where the byte read will be saved */
        pbuffer++;
    }

    /* deselect the flash: chip select high */
    SPI_FLASH_CS_HIGH();
    /* disable the qspi */
    qspi_disable(SPI5);
    /* wait the end of flash writing */
    spi_flash_wait_for_write_end();
}

读写数据比对

if(ERROR == memory_compare(tx_buffer,rx_buffer,256)){
    printf("\n\rErr:Data Read and Write aren't Matching.\n\r");
    is_successful = 1;
}

2. I2S 音频

2.1 简介

I2S(Inter—IC Sound)总线, 又称 集成电路内置音频总线,是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准,该总线专门用于音频设备之间的数据传输,广泛应用于各种多媒体系统。

2.2 源码分析

2.2.1 配置中断

nvic_priority_group_set(NVIC_PRIGROUP_PRE0_SUB4);
nvic_irq_enable(SPI1_IRQn,0,1); 

设置SPI1_IRQn的中断组优先级为0,值子中断优先级1

其中:

void nvic_priority_group_set(uint32_t nvic_prigroup) {
    /* set the priority group value */
    SCB->AIRCR = NVIC_AIRCR_VECTKEY_MASK | nvic_prigroup;
}

关于nvic_irq_enable的说明在《D32450Z评估板的开发》

2.2.2 播放音频文件i2s_audio_play

解析音频文件获取音频属性和数据

errorcode = codec_wave_parsing();
  1. 验证音频数据ID
    • 音频数据的前4个字节是ID
      /* read chunkid, must be 'riff' */
      if(CHUNKID != read_unit(4, bigendian))
          return UNVALID_RIFF_ID;
      

      read_unit(4,big)是从音频数组中读取4个字节,按大端方式组成chunkID

      uint32_t read_unit(uint8_t nbrofbytes, endianness_enum bytesformat) {
        if(littleendian == bytesformat){
            for(index = 0; index < nbrofbytes; index++) 
                temp |= (AUDIOFILEADDRESS[headertab_index++] & 0xFF) << (index * 8);
        }else{
            for(index = nbrofbytes; index != 0; index--) 
                temp |= AUDIOFILEADDRESS[headertab_index++] << ((index-1) * 8);
        }
        return temp;
      }
      

      这里遇到一个BUG: 在小端模式时,wavetestdata[index] = 0xc8 但是wavetestdata[index]<<0会变成0xffffffc8,不清除原因

    • 音频数据区audio_file_address定义
      /* audio start address */
      #define AUDIOFILEADDRESS   (uint32_t)wavetestdata
      

      wavetestdata定义在wave_data.h中,是一个数组,但是它被分配到了0x800_2000的Flash处,大小是948K

3. EXMC SDRAM

3.1 简介

外部存储器控制器EXMC,用来访问各种片外存储器,通过配置寄存器,EXMC可以把AMBA协议转换为专用的片外存储器通信协议,包括SRAM,ROM,NOR Flash,NAND Flash,PC Card和SDRAM。EXMC模块划分为许多个子Bank,每个Bank支持特定的存储器类型,用户可以通过对Bank的寄存器配置来控制外部存储器。

由ARM公司研发推出的AMBA(Advanced Microcontroller Bus Architecture)片上总线协议,提供一种特殊的机制,可将RISC处理器集成在其它IP芯核和外设中,2.0版AMBA标准定义了三组总线:AHB(AMBA高性能总线)、ASB(AMBA系统总线)、和APB(AMBA外设总线)

EXMC由6个模块组成:AHB总线接口,EXMC配置寄存器,NOR/PSRAM控制器,NAND/PC Card控制器,SDRAM控制器和外部设备接口。AHB时钟(HCLK)是参考时钟。

3.1.1 EXMC

framke

EXMC是AHB总线至外部设备协议的转换接口。32位的AHB读写操作可以转化为几个连续的8位或16位读写操作。在数据传输的过程中,AHB数据宽度和存储器数据宽度可能不相同。

3.1.2 外部设备地址映射

bank

EXMC将外部存储器分成多个Bank,每个Bank占256M字节,其中Bank0又分为4个Region,每个Region占64M字节。Bank1和Bank2又都被分成2个Section,分别是属性存储空间和通用存储空间。Bank3分成3个Section,分别是属性存储空间,通用存储空间和I/O存储空间。

  • 每个Bank和Region都有独立的片选控制信号,也都能进行独立的配置
    • Bank0用于访问NOR、PSRAM设备。
    • Bank1和Bank2用于连接NAND Flash,且每个Bank连接一个NAND。
    • Bank3用于连接PC卡。
    • SDRAM Device0和SDRAM Device1用于连接SDRAM

3.1.3 SDRAM 地址映射

HADDR[28]位用来选两个SDRAM Bank

sdram

SDRAM的13位行地址和11位列地址的配置映射:

存储器数据宽度 内部bank 行地址 列地址 最大存储容量
8 bit HADDR[25:24] HADDR[23:11] HADDR[10:0] 64Mbytes: 4 x 8k x 2k
16 bit HADDR[26:25] HADDR[24:12] HADDR[11:1] 128Mbytes: 4 x 8k x 2k x 2
32 bit HADDR[27:26] HADDR[25:13] HADDR[12:2] 256Mbytes: 4 x 8k x 2k x 4

3.1.4 SDRAM 简介

同步动态随机存储器(SDRAM)是通过外部同步时钟刷新的动态随机存储器(DRAM),它的同步时钟由EXMC的EXMC_SDCLK引脚提供,通过配置寄存器EXMC_SDCTLx位SDCLK时钟频率可设置为fhclk/2或者fhclk/3。

SDRAM内部分为多个叫做Bank的区域,允许设备以交错的方式进行访问,以获取更大的并发性和数据传输量。每个Bank可以认为是一个矩阵,其中每个地址对应存储器存储宽度的空间,矩阵由行和列构成,因此存储器的Bank大小可以认为是存储器数据宽度x行数x列数。用户可以通过设置寄存器EXMC_SDCTLx位NBK,SDW,RAW,CAW使EXMC可以与不同的SDRAM进行通信。

由于易失的本征特性,SDRAM需要周期性的刷新。EXMC支持两种刷新模式,自刷新和自动刷新。自刷新是在EXMC挂起的低功耗模式中使用,由SDRAM内部计数提供时钟,内部进行刷新。自动刷新是由EXMC周期性的提供刷新命令。

CAS延迟是读命令和数据线出现第一个可读数据之间的延迟,可以通过寄存器EXMC_SDCTLx位CL设置。

3.1.5 SDRAM 控制器简介

同步动态随机存储器控制器(SDRAMC)是MCU和SDRAM的接口。它把AHB的操作根据SDRAM协议转换为对SDRAM的操作,同时配置寄存器EXMC_SDTCFG满足时序要求

3.2 源码分析

3.2.1 SDRAM外设初始化

init_state = exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);

/*!< SDRAM device0 */
#define EXMC_SDRAM_DEVICE0  ((uint32_t)0x00000004U)   

启用EXMC时钟

rcu_periph_clock_enable(RCU_EXMC);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
rcu_periph_clock_enable(RCU_GPIOD);
rcu_periph_clock_enable(RCU_GPIOE);
rcu_periph_clock_enable(RCU_GPIOF);
rcu_periph_clock_enable(RCU_GPIOG);
rcu_periph_clock_enable(RCU_GPIOH);

fsdram

初始化 EXMC SDRAM

/* config the EXMC access mode */
init_state = exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);

根据数据手册仔细配置各项参数

3.2.2 向SDRAM写入数据

sdram_writebuffer_16(EXMC_SDRAM_DEVICE0,(uint16_t *)txbuffer, 
                     WRITE_READ_ADDR, BUFFER_SIZE/2);

#define EXMC_SDRAM_DEVICE0   ((uint32_t)0x00000004U) 
#define WRITE_READ_ADDR            ((uint32_t)0x0000)
#define BUFFER_SIZE                ((uint32_t)0x0400)
  • 根据SDRAM设备确定其基地址
      if(sdram_device == EXMC_SDRAM_DEVICE0){
          temp_addr = SDRAM_DEVICE0_ADDR;
      }else{
          temp_addr = SDRAM_DEVICE1_ADDR;
      }
    

    其中:

      #define SDRAM_DEVICE0_ADDR   ((uint32_t)0xC0000000)
      #define SDRAM_DEVICE1_ADDR   ((uint32_t)0xD0000000)
    
  • 直接向SDRAM中写入数据,依次2个字节
       /* While there is data to write */
      for(; numtowrite != 0; numtowrite--) {
          /* Transfer data to the memory */
          *(uint16_t *) (temp_addr + write_addr_prt) = *pbuffer++;
    
          /* Increment the address */  
          write_addr_prt += 2;
      }
    

3.2.3 从SDRAM中读取数据

sdram_readbuffer_16(EXMC_SDRAM_DEVICE0,(uint16_t *)rxbuffer, 
                     WRITE_READ_ADDR, BUFFER_SIZE/2)
{
  ...

  for(; numtowrite != 0; numtowrite--){
      /* read a byte from the memory */
      *pbuffer++ = *(uint16_t*) (temp_addr + write_addr_prt);
  
      /* increment the address */
      write_addr_prt += 2;
  }
}

4. SD 卡

4.1 简介

安全的数字输入/输出接口(SDIO)定义了 SD 卡、SD I/O 卡、多媒体卡(MMC)和 CE-ATA 卡主机接口,提供 APB2 系统总线与 SD 存储卡、SD I/O 卡、MMC 和 CE-ATA 设备之间的数据传输。

4.2 源码分析

4.2.1 SD卡初始化

启用GPIO、SDIO和DMA1时钟

static void rcu_config(void) {
    rcu_periph_clock_enable(RCU_GPIOC);
    rcu_periph_clock_enable(RCU_GPIOD);
    
    rcu_periph_clock_enable(RCU_SDIO);
    rcu_periph_clock_enable(RCU_DMA1);
}

sdio

5. 以太网 TcpUdp

5.1 简介

该以太网模块包含10/100Mbps以太网MAC(媒体访问控制器) ,采用DMA优化数据帧的发送与接收性能,支持MII(媒体独立接口)与RMII(简化的媒体独立接口)两种与物理层(PHY)通讯的标准接口,实现以太网数据帧的发送与接收。

5.1.1 模块框图

以太网模块由MAC(介质访问控制器)模块、MII/RMII模块和一个以描述符形式控制的DMA模块组成。

eth

在使用以太网模块时,AHB的频率应至少为25MHz

  • MAC模块通过MII或RMII与片外PHY连接
    • 通过对SYSCFG_CFG1寄存器的相关位进行设置, 可以选择使用哪种接口
  • SMI(站点管理接口)用于配置和管理外部PHY

  • 发送数据模块包括:
    • TxDMA控制器
      • 从存储器中读取描述符和数据,以及将状态写入存储器
    • TxMTL
      • 对发送数据的控制,管理和存储
      • TxMTL内含TxFIFO,用于缓存待MAC发送的数据
    • MAC发送控制寄存器组,用于管理和控制数据帧的发送
  • 接收数据模块包括
    • RxDMA控制器
      • 从存储器中读取描述符,以及将数据与状态写入存储器
      • RxMTL
        • 对接收数据的控制,管理和存储
        • RxMTL实现了RxFIFO,用于存储待转发到系统存储的帧数据
      • MAC接收控制寄存器组
        • 管理数据帧的接收和标示接收状态
        • MAC内含接收过滤器,采用多种过滤机制,滤除特定的以太网帧

enet1

5.1.2 MAC 802.3 以太网数据包描述

MAC的数据通信可使用两种帧格式:

  • 基本的MAC帧格式;
  • 带标签的MAC帧格式(对基本的MAC帧格式的扩展)

mac

注意:

  1. 以太网控制器发送每个字节时都按照低位先出的次序进行传输
  2. CRC计算包括帧数据的所有字节除去前导码和帧首界定码域
  3. 以太网帧的32位CRC生成多项式为0x04C11DB7

5.1.3 功能描述

5.1.3.1 接口配置

  • 以太网模块通过MII/RMII接口与片外PHY连接,传送与接收以太网包
  • MII或RMII模式由软件选择并通过SMI接口对PHY进行管理
  • SMI:站点管理接口
    • SMI用于访问和设置PHY的配置
    • 可以通过其访问任意PHY的任意寄存器
    • MDC时钟线和MDIO数据线具体作用如下
      • MDC:最高频率为2.5MHz的时钟信号
      • MDIO:用于与PHY之间的数据传输,与MDC时钟线配合,接收/发送数据
    • SMI 写操作
    • SMI 读操作
  • MII/RMII 的选择
    • 当以太网控制器处于复位状态或者在使能时钟前
    • 通过对SYSCFG_CFG1寄存器的ENET_PHY_SEL位进行配置
    • 选择使用MII或者RMII模式,默认为MII模式
  • MII:媒体独立接口
    • 媒体独立接口(MII)用于MAC与外部PHY互联,支持10Mbit/s和100Mbit/s的数据传输模式 mii
  • RMII:精简媒体独立接口
    • 精简媒体独立接口(RMII)规范减少了以太网通信所需要的引脚数
    • MII接口需要16个引脚用于数据和控制信号,而RMII标准则将引脚数减少到了7个 rmii
    • RMII具有以下特性
      • 只有一个时钟信号,且该时钟信号需要提高到50MHz
      • MAC和外部的以太网PHY需要使用同样的时钟源
      • 使用2位宽度的数据收发

5.1.3.2 MAC 功能简介

MAC模块能够实现以下功能:

  • 数据封装(传送和接收)帧
    • 帧检测/解码、帧边界定界
    • 寻址(管理源地址和目标地址)
    • 错误检测
  • 介质访问管理(半双工模式下)
    • 介质分配(防止冲突)
    • 冲突解决(处理冲突)
  • MAC模块可以在两种模式下工作
    • 半双工模式
    • 全双工模式

5.2 源码分析

5.2.1 创建task任务

// 设置组优先级0
nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0);

/* init task */
xTaskCreate(init_task, "INIT", configMINIMAL_STACK_SIZE * 2, NULL, INIT_TASK_PRIO, NULL);

/* start scheduler */
vTaskStartScheduler();

5.2.1.1 创建并启动调度

xTaskCreate(init_task, "INIT", 
            configMINIMAL_STACK_SIZE * 2, 
            NULL, INIT_TASK_PRIO, NULL);

/* start scheduler */
vTaskStartScheduler();

while(1){
}

5.2.2 init_task 初始化函数

5.2.2.1 配置以太网

配置GPIO、时钟、MAC、DMA

enet_system_setup()
  1. nvic_configuration配置网络中断
     static void nvic_configuration(void) {
         nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x0);
         nvic_irq_enable(ENET_IRQn, 2, 0);
     }
    

    这里nvic_vector_table_set是配置系统中断向量表的地址

     nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x0);
    
     #define NVIC_VECTTAB_FLASH   ((uint32_t)0x08000000) 
    

    将中断向量表的地址设置为0x0800_0000,本质是设置VTOR

     SCB->VTOR = nvic_vict_tab | (offset & NVIC_VECTTAB_OFFSET_MASK);
    

    配置好中断向量表的地址后,启用GD32对ENET中断请求的响应,并设置好中断的优先级

  2. 配置网络所需的GPIO
     enet_gpio_config();
    
    • 接好跳线
      jp
    • 设置GPIO
      gpoi
      • GPIA[8] 配置成 push/pull 模式
    • 启用系统配置器RCU_SYSCFG
    • 选择DIV2,在CKOUT0引脚(PA8)上从200MHz获得50MHz的时钟,用于PHY的时钟
      rcu_ckout0_config(RCU_CKOUT0SRC_PLLP, RCU_CKOUT0_DIV4);
      
      #define RCU_CFG0_CKOUT0DIV    BITS(24,26) 
      #define RCU_CFG0_CKOUT0SEL    BITS(21,22)               
      #define RCU_CKOUT0SRC_PLLP    CFG0_CKOUT0SEL(3) 
      #define RCU_CKOUT0_DIV4       CFG0_CKOUT0DIV(6)                  
      
      void rcu_ckout0_config(uint32_t ckout0_src, uint32_t ckout0_div){
        reg &= ~(RCU_CFG0_CKOUT0SEL | RCU_CFG0_CKOUT0DIV );
        RCU_CFG0 = (reg | ckout0_src | ckout0_div);
      }
      

      | 位 | 名称 | 说明 | | —– | ————– | ————————————– | | 26:24 | CKOUT0DIV[2:0] | 110:CK_OUT0 被 4 分频 | | 22:21 | CKOUT0SEL[1:0] | CKOUT0时钟源选择
      11:选择CK_PLLP时钟 |

    • 配置PHY
      syscfg_enet_phy_interface_config(SYSCFG_ENET_PHY_RMII){
        reg = SYSCFG_CFG1;
        reg &= ~SYSCFG_CFG1_ENET_PHY_SEL;
        SYSCFG_CFG1 = (reg | syscfg_enet_phy_interface);
      }
      
      #define SYSCFG_CFG1_ENET_PHY_SEL    BIT(23)   
      /*!< RMII is selected for the Ethernet MAC */
      #define SYSCFG_ENET_PHY_RMII   ((uint32_t)0x00800000U)   
      

      配置寄存器 1 (SYSCFG_CFG1)

      地址偏移:0x04 复位值:0x0000 0000

      名称 说明
      23 ENET_PHY_SEL 以太网PHY选择
      这些位为以太网MAC选择PHY接口
      当以太网MAC在复位状态下,且在MAC时钟使能之前,这些控制位必须被配置
      0:选择MII
      1:选择RMII
    • 根据原理图配置GPIO
      /* PA1: ETH_RMII_REF_CLK */
      gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_1);
      gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_200MHZ,GPIO_PIN_1);
            
      /* PA2: ETH_MDIO */
      gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_2);
      gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_200MHZ,GPIO_PIN_2);
            
      /* PA7: ETH_RMII_CRS_DV */
      gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7);
      gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_200MHZ,GPIO_PIN_7);   
            
      gpio_af_set(GPIOA, GPIO_AF_11, GPIO_PIN_1);
      gpio_af_set(GPIOA, GPIO_AF_11, GPIO_PIN_2);
      gpio_af_set(GPIOA, GPIO_AF_11, GPIO_PIN_7);
      

5.2.2.2 配置LwIP栈

  1. 初始化堆内存

    将堆清零,并初始化start、end和 lowest-free

     mem_init() {
       // 堆内存大小必须是4字节框定
       LWIP_ASSERT("Sanity check alignment",
         (SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0);
     }
    
     #define SIZEOF_STRUCT_MEM    LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))
     #define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1) & ~(MEM_ALIGNMENT-1))
    

    这里SIZEOF_STRUCT_MEM是4字节框定后的struct mem大小

    • 技巧1:4字节框定的内存大小
      MEM_ALIGNMENT = 4
      def LWIP_MEM_ALIGN_SIZE(size):
        return (((size) + MEM_ALIGNMENT - 1) & ~(MEM_ALIGNMENT-1))
      
      >>> LWIP_MEM_ALIGN_SIZE(30)
      32
      >>> LWIP_MEM_ALIGN_SIZE(33)
      36
      
    • 技巧2:4字节对齐
      #define LWIP_MEM_ALIGN(addr) \
           ((void *)(((mem_ptr_t)(addr) + MEM_ALIGNMENT - 1) & ~(mem_ptr_t)(MEM_ALIGNMENT-1)))
      
    • 堆首地址对齐
      ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
      
      // 我们需要在结尾处有一个mem结构并为对齐提供一些空间
      u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT];
      #define LWIP_RAM_HEAP_POINTER ram_heap
      #define MEM_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MEM_SIZE)
      #define MEM_SIZE             (20*1024)                
      struct mem {
        /** index (-> ram[next]) of the next struct */
        mem_size_t next;
        /** index (-> ram[prev]) of the previous struct */
        mem_size_t prev;
        /** 1: this area is used; 0: this area is unused */
        u8_t used;
      };
      
    • 初始化首尾堆
      mem = (struct mem *)(void *)ram;
      mem->next = MEM_SIZE_ALIGNED;
      mem->prev = 0;
      mem->used = 0;
      
      ram_end = (struct mem *)(void *)&ram[MEM_SIZE_ALIGNED];
      ram_end->used = 1;
      ram_end->next = MEM_SIZE_ALIGNED;
      ram_end->prev = MEM_SIZE_ALIGNED;
      
    • 初始化指针
      /* initialize the lowest-free pointer to the start of the heap */
      lfree = (struct mem *)(void *)ram;
      

5.2.2.3 初始化TCP服务器

/* initilaize the tcp server: telnet 8000 */
hello_gigadevice_init();
  1. 创建一个新的TCP控制块
     pcb = tcp_new();
    

    这只是通过malloc分配一个TCP控制块并初始化一下

  2. 将它绑定到8000的端口
     tcp_bind(pcb, IP_ADDR_ANY, 8000);
    

    本质是

     pcb->local_ip = IP_ADDR_ANY;
     pcb->local_port = port;
     TCP_REG(&tcp_bound_pcbs, pcb);
    
     #define IP_ADDR_ANY 0
    

    把这个pcb挂接到tcp_bound_pcbs链表上

  3. 设置这个TCP为监听状态
     pcb = tcp_listen(pcb);
    

    本质是通过memp_malloc创建一个lpcb,根据pcb初始化lpcb,最后

     TCP_REG(&tcp_listen_pcbs.pcbs, (struct tcp_pcb *)lpcb);
    
  4. 连接建立后指定处理函数
     tcp_accept(pcb, hello_gigadevice_accept);
    

5.2.2.4 初始化TCP客户端

/* initilaize the tcp client: echo 1026 */
tcp_client_init();
  1. 构建ip地址
      IP4_ADDR(&ipaddr, IP_S_ADDR0, IP_S_ADDR1, IP_S_ADDR2, IP_S_ADDR3);
    

    其中:

      #define IP4_ADDR(ipaddr, a,b,c,d) \
           (ipaddr)->addr = ((u32_t)((d) & 0xff) << 24) | \
                            ((u32_t)((c) & 0xff) << 16) | \
                            ((u32_t)((b) & 0xff) << 8)  | \
                             (u32_t)((a) & 0xff)
    
  2. 分配一个新TCP控制块
      pcb = tcp_new();
    
  3. 连接服务器
      tcp_bind(pcb, IP_ADDR_ANY, 1026)
      tcp_connect(pcb, &ipaddr, 1026, tcp_client_connected);
    

    tcp_connect的本质

      pcb->remote_port = port;
      pcb->rcv_nxt = 0;
      pcb->snd_nxt = iss;
      pcb->lastack = iss - 1;
      ...
      pcb->connected = connected;
    

6. 复位和时钟单元(RCU)

目的:

  1. 搞明白为什么BSP的高速晶振频率是12MHz而demo中的是25MHz
    • 电路外接的晶振决定
  2. 怎么控制系统复位
    • 通过寄存器进行系统复位
    • 外部引脚复位 (NRST)
    • 窗口看门狗计数终止 (WWDGT_RSTn)
    • 独立看门狗计数终止 (FWDGT_RSTn)
    • Cortex®-M4的中断应用和复位控制寄存器中的SYSRESETREQ位置‘1’ (SW_RSTn)
  3. 实现baseboot在GD32450z板上的移植
    • UART轮询串口移植

6.1 复位控制单元(RCTL)

6.1.1 复位种类

GD32F4xx复位控制包括三种控制方式:电源复位、系统复位和备份域复位

  • 电源复位
    • 冷复位,复位除了备份域的所有系统
  • 系统复位
    • 复位除了SW-DP控制器(JTAP-DP和SW-DP:debug port)和备份域之外的部分
    • 包括处理器内核和外设IP
  • 备份区复位
    • 复位备份区域

6.1.2 功能说明

6.1.2.1 电源复位

  • 信号来源
    • 上电/掉电复位(POR/PDR 复位)
    • 欠压复位(BOR 复位)
    • 从待机模式中返回后由内部复位发生器产生
  • 复位范围
    • 复位除了备份域的所有的寄存器
  • 复位电平
    • 电源复位为低电平有效
    • 当内部LDO电源基准准备好提供1.2V电压时,电源复位电平将变为无效

      LDO即low dropout regulator,是一种低压差线性稳压器

6.1.2.2 系统复位

reset

  • 信号来源
    • 上电复位 (POWER_RSTn)
    • 外部引脚复位 (NRST)
    • 窗口看门狗计数终止 (WWDGT_RSTn)
    • 独立看门狗计数终止 (FWDGT_RSTn)
    • Cortex®-M4的中断应用和复位控制寄存器中的SYSRESETREQ位置‘1’ (SW_RSTn)
    • 用户选择字节寄存器nRST_STDBY位设置为0并且进入待机模式时将产生复位(OB_STDBY_RSTn)
    • 用户选择字节寄存器 nRST_DPSLP 设置为0, 并且进入深度睡眠模式时 (OB_DPSLP_RSTn)

6.1.2.3. 备份域复位

信号来源:

  • 设置备份域控制寄存器中的BKPRST位为‘1’
  • 备份域电源上电复位(在V_DD和V_BAT两者都掉电的前提下,V_DD或V_BAT上电)

6.2 时钟控制单元(CCTL)

6.2.1 简介

6.2.1.1 CCTL提供的频率功能

  • 内部频率功能
    • 内部16M RC振荡器时钟(IRC16M)
    • 内部48M RC振荡器时钟(IRC48M)
    • 内部32K RC振荡器时钟(IRC32K)
  • 外部频率功能
    • 外部高速晶体振荡器时钟(HXTAL)
    • 外部低速晶体振荡器时钟(LXTAL)
  • 频率控制功能
    • 三个锁相环(PLL)
    • HXTAL时钟监视器
    • 时钟预分频器
    • 时钟多路复用器
    • 时钟门控电路

6.2.1.2 系统时钟及看门狗、RTC时钟

  • 系统时钟(CK_SYS)
    • 提供给AHB、APB和Cortex®-M4时钟
    • 来源
      • 内部IRC16M
      • HXTAL
      • PLL
    • MAX频率:200MHz
  • 独立看门狗
    • 独立的时钟源(IRC32K)
  • 实时时钟(RTC)来源
    • IRC32K
    • LXTAL 或 HXTAL 的分频
      • RCU_CFG0寄存器的RTCDIV位控制

6.2.2 时钟预分频器

预分频器可以配置 AHB(Max: 200MHz)、APB2(Max: 100MHz) 和 APB1(Max: 50MHz) 域的时钟频率。

  • SysTick时钟
    • RCU通过AHB(200MHz)时钟(HCLK)8分频后作为Cortex系统定时器(SysTick)的外部时钟
    • 通过对SysTick控制和状态寄存器的设置,可选择上述时钟或AHB(HCLK)时钟作为SysTick时钟
  • ADC时钟
    • 寄存器
      • 通过设置ADC_SYNCCTL寄存器的ADCCK位来选择
    • 时钟源
      • APB2时钟经2、4、6、8分频
      • AHB时钟经5、6、10、20分频
  • TIMER时钟
    • AHB时钟分频
  • USBFS/USBHS/TRNG/SDIO的时钟
    • 时钟源
      • 由CK48M时钟提供
    • CK48M时钟源
      • 寄存器
        • 配置RCU_ADDCTL寄存器的CK48MSEL及PLL48MSEL位
      • 时钟源
        • 选择PLLQ时钟
        • PLLSAIP时钟
        • IRC48M时钟
  • CTC时钟
    • IRC48M时钟提供
    • 通过CTC单元,可以实现IRC48M时钟精度的自动调整
  • I2S时钟
    • 寄存器
      • 配置寄存器RCU_CFG0的I2SSEL位
    • 时钟源
      • 由PLLI2SR时钟
      • 外部I2S_CKIN引脚输入时钟
  • TLI时钟
    • 寄存器
      • RCU_CFG1的PLLSAIRDIV位域
    • 时钟源
      • PLLSAIR时钟的2、4、8、16分频提供
  • 以太网TX/RX时钟
    • 寄存器
      • SYSCFG_CFG1寄存器的ENET_PHY_SEL位
    • 时钟源
      • 由外部引脚(ENET_TX_CLK / ENET_RX_CLK)输入时钟提供
  • RTC时钟
    • 寄存器
      • RCU_BDCTL寄存器的RTCSRC位
    • 时钟源
      • LXTAL时钟
      • IRC32K时钟或HXTAL时钟的2-31分频提供
        • 分频值:RCU_CFG0寄存器的RTCDIV位域值决定
  • FWDG时钟
    • 强制选择由IRC32K时钟做为时钟源

6.2.3 外部高速晶体振荡器时钟(HXTAL)

  1. 4到32MHz的外部高速晶体振荡器可为系统时钟提供更为精确时钟源
  2. 启用HxTAL晶振
    • 设置控制寄存器RCU_CTL的HXTALEN位来启动或关闭
  3. 晶振稳定
    • 在控制寄存器RCU_CTL中的HXTALSTB位用来指示外部高速振荡器是否已稳定
    • 直到这一位被硬件置‘1’,时钟才被释放出来
    • 晶振启动时间
    • 这个特定的延迟时间被称为振荡器的启动时间
  4. 晶振稳定中断
    • 在中断寄存器RCU_INT中的相应中断使能位HXTALSTBIE位被置‘1’,将会产生相应中断

6.2.4 内部 16M RC 振荡器时钟(IRC16M)

  1. 属性
    • 拥有16MHz的固定频率
    • 设备上电后CPU默认选择其做为系统时钟源
  2. 启用和关闭
    • 控制寄存器(RCU_CTL)中的IRC16MEN位被启动和关闭
  3. 晶振稳定
    • 控制寄存器RCU_CTL中的IRC16MSTB位用来指示IRC16M内部RC振荡器是否稳定
  4. 晶振稳定中断
    • RCU_INT中的相应中断使能位IRC16MSTBIE被置‘1’,在IRC16M稳定以后,将产生一个中断

6.2.5 内部 48M RC 振荡器时钟(IRC48M)

  • 属性
    • 48MHz 的固定频率
    • 为USBFS/USBHS/TRNG/SDIO模块提供成本更低的时钟源
  • 启动和关闭
    • RCU_ADDCTL[IRC48MEN] = 1
  • 稳定
    • RCU_ADDCTL[IRC48MSTB]
  • 中断使能
    • RCU_ADDINT[IRC48MSTBIE] = 1
  • 辅助
    • 因为USB模块需要的时钟频率必须满足48MHz±1%
    • CTC单元提供了一种硬件自动执行动态调整的功能将IRC48M时钟调整到需要的频率

6.2.6 锁相环(PLL)

  • 属性
    • 三个内部锁相环
      • PLL
      • PLLI2S
      • PLLSAI
    • PLL
      • PLLP:可做为系统时钟(不超过200MHz)
      • PLLQ:可以做为USBFS/USBHS/TRNG/SDIO模块的时钟源
    • PLLI2S
      • PLLI2S时钟可以做为I2S模块的时钟源
    • PLLSAI
      • 可以做为CK48M或TLI模块的时钟源
  • PLL
    • 启动和关闭
      • RCU_CTL[PLLEN] = 1
    • 稳定
      • RCU_CTL[PLLSTB]
    • 中断使能
      • RCU_INT[PLLSTBIE] = 1
  • PLLI2S
    • RCU_CTL[PLLI2SEN]
    • RCU_CTL[PLLI2SSTB]
    • RCU_INT[PLLI2SSTBIE]
  • PLLSAI
    • RCU_CTL[PLLSAIEN]
    • RCU_CTL[PLLSAISTB]
    • RCU_INT[PLLSAISTBIE]
  • 注意
    • 当进入Deepsleep/Standby模式或者HXTAL监视器检测到时钟阻塞时(HXTAL做为锁相环的输入时钟),这三路PLL将被关闭

6.2.7 外部低速晶体振荡器时钟(LXTAL)

  • 属性
    • LXTAL是一个频率为32.768kHz的外部低速晶体或陶瓷谐振器
  • 启用
    • RCU_BDCTL[LXTALEN]
    • BDCTL:备份域控制寄存器
  • 稳定
    • RCU_BDCTL[LXTALSTB]
  • 中断
    • RCU_INT[LXTALSTBIE]

6.2.8 系统时钟(CK_SYS)选择

  • 属性
    • 系统复位后,IRC16M时钟默认做为CK_SYS的时钟源
  • 时钟源选择
    • RCU_CFG0[SCS]可以选择系统时钟源为HXTAL或CK_PLLP
  • 时钟源切换过程
    • 当SCS的值被改变,系统时钟将使用原来的时钟源继续运行直到转换的目标时钟源稳定
    • 当一个时钟源被直接或通过PLL间接作为系统时钟时,它将不能被停止

6.2.9 HXTAL 时钟监视器(CKM)

  • 启用
    • RCU_CTL[CKMEN]
    • 该功能必须在HXTAL启动延迟完毕后使能
    • 在HXTAL停止后禁止
  • 功能
    • 一旦监测到HXTAL故障,HXTAL将自动被禁止
    • HXTAL故障将促使选择IRC16M为系统时钟源

6.2.10 时钟输出能力

  • 属性
    • 时钟输出功能输出从32kHz到200MHz的时钟
  • 输出信号
      • RCU_CFG0[CKOUT0SEL]可以选择CK_OUT0输出的时钟信号
      • RCU_CFG0[CKOUT1SEL]可以选择CK_OUT1输出的时钟信号
    • 分频
      • RCU_CFG0[CKOUT0DIV]分频
      • RCU_CFG0[CKOUT1DIV]分频
    • 相应的GPIO引脚应该被配置成备用功能I/O(AFIO)模式来输出选择的时钟信号

7. FMC 双 BANK 启动切换

7.1 简介

闪存控制器(FMC),提供了片上闪存需要的所有功能。在闪存的前512K字节空间内,CPU执行指令零等待。FMC也提供了扇区擦除,整片擦除,以及32位整字或16位半字、字节编程等闪存操作。

7.1.1 主要特征

  • 高达3072KB字节的片上闪存可用于存储指令或数据
  • 对于GD32F4xx,使用了两片闪存
    • 前1024KB容量在第0片闪存(bank0)中
    • 后续的容量在第1片闪存(bank1)中
  • 支持32位整字或16位半字、字节编程,扇区擦除和整片擦除操作
  • 2个大小为16字节的选项字节可根据用户需求配置
  • 512字节OTP块和16字节OTP锁定块(一次性编程),用于存储用户数据
  • 30K字节信息块(一次性编程),用于引导装载程序
  • 选项字节会在每次系统复位时装载到选项字节控制寄存器
  • 在闪存的前512K字节空间内,CPU执行指令零等待
    • 在此范围外,CPU读取指令存在较长延时

7.1.2 存储结构

Flash块 名称 地址范围 大小
块0(1MB) 扇区0 0x0800_0000 ~ 0x0800_3FFF 16KB
  扇区1 0x0800_4000 ~ 0x0800_7FFF 16KB
  扇区2 0x0800_8000 ~ 0x0800_BFFF 16KB
  扇区3 0x0800_C000 ~ 0x0800_FFFF 16KB
  扇区4 0x0801_0000 ~ 0x0801_FFFF 64KB
  扇区5 0x0802_0000 ~ 0x0803_FFFF 128KB
  128KB
  扇区11 0x080E_0000 ~ 0x080F_FFFF 128KB
块1(2MB) 扇区12 0x0810_0000 ~ 0x0810_3FFF 16KB
  16KB
  扇区15 0x0810_C000 ~ 0x0810_FFFF 16KB
  扇区16 0x0811_0000 ~ 0x0811_FFFF 64KB
  扇区17 0x0812_0000 ~ 0x0813_FFFF 128KB
  128KB
  扇区23 0x081E_0000 ~ 0x081F_FFFF 128KB
  扇区24 0x0820_0000 ~ 0x0823_FFFF 256KB
  256KB
  扇区27 0x082C_0000 ~ 0x082F_FFFF 256KB
信息块(iROM) 引导装载程序 0x1FFF_0000 ~ 0x1FFF_77FF 30KB
一次性编程块 一次性编程区域 0x1FFF_7800 ~ 0x1FFF_7A0F 528字节
选项字节块 块0 0x1FFF_C000 ~ 0x1FFF_C00F 16字节
  块1 0x1FFE_C000 ~ 0x1FFE_C00F 16字节

7.1.3 扇区操作

7.1.3.1 扇区擦除

FMC的扇区擦除功能使得主存储闪存的扇区内容初始化为高电平

7.1.3.2 整片擦除

整片擦除功能可以初始化主存储闪存块的内容,当设置MER0为1时,擦除过程仅作用于Bank0,当设置MER1为1时,擦除过程仅作用于Bank1,当设置MER0和MER1为1时,擦除过程作用于整片闪存

7.1.3.3 主存储闪存块编程

FMC提供了一个32位整字/16位半字/8位字节编程功能,用来修改主存储闪存块内容

7.1.3.4 OTP 闪存块编程

FMC提供了一个32位整字/16位半字/8位字节编程功能,用来修改OTP闪存块内容,OTP闪存块不可进行擦除操作

7.1.3.5 选项字节修改

FMC提供了一个擦除功能用来修改闪存中的选项字节,每次系统复位后,闪存的选项字节被重加载到FMC_OBCTL0和FMC_OBCTL1寄存器后,可选字节生效

7.2 源码分析

7.2.1 根据启用块设置中断向量表地址

#ifndef BB_ENABLE
    nvic_vector_table_set(NVIC_VECTTAB_FLASH,0x000000);
#else
    nvic_vector_table_set(NVIC_VECTTAB_FLASH,0x100000);
#endif

BB是选项字节中的选项,其内容如下:

地址 名称 说明
0x1FFF_C000 USER [7] nRST_STDBY
0:进入待机模式时产生复位
1:进入待机模式时不产生复位
    [6] nRST_DPSLP
0:进入深度睡眠模式时产生复位
1:进入深度睡眠模式时不产生复位
    [5] nWDG_HW
0:硬件自由看门狗
1:软件自由看门狗
    [4] BB
0:当配置从主存储块启动(默认值)时,从bank0启动
1:当配置从主存储块启动时,若bank1无启动程序, 从bank0启动。否则,从bank1启动
nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x100000) {
    SCB->VTOR = nvic_vict_tab | (offset & NVIC_VECTTAB_OFFSET_MASK);
}

/*!< Flash base address */
#define NVIC_VECTTAB_FLASH   ((uint32_t)0x08000000) 
/*!< RAM base address */
#define NVIC_VECTTAB_RAM     ((uint32_t)0x20000000) 
/* set the NVIC vector table offset mask */
#define NVIC_VECTTAB_OFFSET_MASK    ((uint32_t)0x1FFFFF80)
  • VTOR寄存器存放的是中断向量表的起始地址
    • 启用了引导块,则中断向量表的起始地址为:0x0810_0000(Bank1)
    • 没有启用引导块,中断向量表的起始地址为:0x0800_0000(Bank0)

设定向量表的基地址,因为系统上电序列或系统复位后,ARM® Cortex®-M4处理器先从0x0000_0000地址获取栈顶值,再从 0x0000_0004地址获得引导代码的基地址,然后从引导代码的基地址开始执行程序。

  • 自定义的引导源对应的存储空间会被映射到引导存储空间,即从0x0000_0000开始的地址空间
  • 如果片上SRAM(开始于0x2000_0000的存储空间)被选为引导源,用户必须在应用程序初始化代码中通过修改NVIC异常向量表和偏移地址将向量表重置到SRAM中
  • 当主FLASH存储器被选择作为引导源,从0x0800_0000开始的存储空间会被映射到引导存储空间
    • 由于主FLASH存储器的Bank0或Bank1均可映射到地址 0x0800_0000(通过配置SYSCFG_CFG0寄存器FMC_SWP控制位,具体参考1.5.1 节 ),所以,微控制器可以使用该方法从Bank0或Bank1中启动。
    • 为了使能引导块功能,选项字节中的BB控制位需要被置位
      • 当该控制位被置位并且主FLASH存储器被选择作为引导源
      • 微控制器从引导装载程序中启动并且引导装载程序跳至主FLASH存储器的Bank1中执行代码
      • 在应用程序初始化代码中,用户必须通过修改NVIC异常向量表和偏移地址将向量表重置到Bank1基地址
    • 引导装载程序在生产器件的过程中已经被编程,用于通过以下其中一个通信接口重新编程主FLASH存储器:
      • USART0(PA9和PA10)
      • USART2(PB10和PB11或PC10和PC11)

7.2.2 解锁选项字节

/* unlock the option byte operation */
ob_unlock() {
  // 若锁定
  if(RESET != (FMC_OBCTL0 & FMC_OBCTL0_OB_LK)){
      /* write the FMC key */
      FMC_OBKEY = OB_UNLOCK_KEY0;
      FMC_OBKEY = OB_UNLOCK_KEY1;
  }
}

/*!< FMC option byte control register 0 */
#define FMC_OBCTL0    REG32((FMC) + 0x0014U) 
/*!< FMC_OBCTL0 lock bit */
#define FMC_OBCTL0_OB_LK   BIT(0) 
/*!< FMC option byte control register 1 */
#define FMC_OBCTL1    REG32((FMC) + 0x0018U)
/*!< FMC option byte unlock key register */
#define FMC_OBKEY     REG32((FMC) + 0x0008U)

/*!< ob unlock key 0 */
#define OB_UNLOCK_KEY0  ((uint32_t)0x08192A3BU) 
/*!< ob unlock key 1 */
#define OB_UNLOCK_KEY1  ((uint32_t)0x4C5D6E7FU)        

7.2.2.1 选项字节控制寄存器 0/1 (FMC_OBCTL0/1)

名称 说明
0 OB_LK FMC_OBCTLx锁定位
当往FMC_OBKEY寄存器写值顺序正确时,该位由硬件清0。软件置位

7.2.2.2 选项字节解锁寄存器 (FMC_OBKEY)

地址偏移:0x08 复位值:0x0000 0000 该寄存器只能按字(32位)访问

名称 说明
31:0 OBKEY[31:0] FMC_OBCTLx选项字节解锁寄存器
这些位仅能被软件写
写解锁值到OBKEY[31:0]解锁FMC_OBCTLx寄存器的选项字节命令

FMC_CTL/FMC_OBCTLx 寄存器解锁

  • FMC_CTL
    • 复位后,FMC_CTL寄存器进入锁定状态,LK位置为1
    • 通过先后向FMC_KEY寄存器写入0x45670123和0xCDEF89AB,可以使得FMC_CTL解锁。两次写操作后,FMC_CTL寄存器的LK位被硬件清0
    • 可以通过软件设置FMC_CTL寄存器的LK位为1再次锁定FMC_CTL寄存器
  • FMC_OBCTLx
    • FMC_OBCTL0寄存器,在FMC_CTL被解锁后仍然处于被保护状态
    • 解锁过程为两次写操作,向FMC_OBKEY寄存器先后写入0x08192A3B和0x4C5D6E7F,然后硬件将FMC_OBCTL0寄存器中的OB_LK位清零
    • 软件可以将FMC_OBCTLx的OB_LK位置1来锁定FMC_OBCTLx

7.2.3 清除FMC错误标志

/* clear pending flags */
fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_OPERR | FMC_FLAG_WPERR | FMC_FLAG_PGMERR | FMC_FLAG_PGSERR) {
    FMC_STAT = fmc_flag;
}

/*!< FMC status register */
#define FMC_STAT    REG32((FMC) + 0x000CU)

| 标志 | 说明 | | ————— | ——————————————- | | FMC_FLAG_RDDERR | FMC read D-bus protection error flag bit | | FMC_FLAG_PGSERR | FMC program sequence error flag bit | | FMC_FLAG_PGMERR | FMC program size not match error flag bit | | FMC_FLAG_WPERR | FMC erase/program protection error flag bit | | FMC_FLAG_OPERR | FMC operation error flag bit | | FMC_FLAG_END | FMC end of operation flag bit |

7.2.4 配置BB

if(RESET == (FMC_OBCTL0 & FMC_OBCTL0_BB)){
    ob_boot_mode_config(OB_BB_ENABLE);
    printf("\r\n Set BB bit and then restart from bank1\r\n");
}else{
    ob_boot_mode_config(OB_BB_DISABLE);
    printf("\r\n Reset BB bit and then restart from bank0\r\n");
}

/*!< option byte boot bank value */
#define FMC_OBCTL0_BB   BIT(4) 
名称 说明
4 BB 选项字节启动块
0: Bank0
1: Bank1,若bank1无启动程序,从bank0启动。
// 设置FMC_OBCTL0[BB] = boot_mode
void ob_boot_mode_config(uint32_t boot_mode) {
    uint32_t reg;
    
    reg = FMC_OBCTL0;
    /* set option byte boot bank value */
    reg &= ~FMC_OBCTL0_BB;
    FMC_OBCTL0 = (reg | boot_mode);
}

7.2.5 发送选项字节修改命令

/* send option byte change command */
ob_start();
/* lock the option byte operation */
ob_lock();

其中

void ob_lock(void) {
    /* reset the OB_LK bit */
    FMC_OBCTL0 |= FMC_OBCTL0_OB_LK;
}
void ob_start(void) {
    fmc_state_enum fmc_state = FMC_READY;
    /* set the OB_START bit in OBCTL0 register */
    FMC_OBCTL0 |= FMC_OBCTL0_OB_START;
    // 等待FMC就绪
    fmc_state = fmc_ready_wait(FMC_TIMEOUT_COUNT);
    if(FMC_READY != fmc_state){
        while(1){
        }
    }
}

/*!< send option byte change command to FMC bit */
#define FMC_OBCTL0_OB_START   BIT(1)
名称 说明
1 OB_START 发送选项字节命令到FMC
该位由软件设置。当BUSY位清0时由硬件清除该位
fmc_state_enum fmc_ready_wait(uint32_t timeout)

#define FMC_STAT    REG32((FMC) + 0x000CU)

本质就是读取FMC_STAT寄存器,得到FMC_STAT & FMC_FLAG_BUSY ?= 1,其中RMC_FLAG_BUSY = 1<<16

状态寄存器 (FMC_STAT)

地址偏移:0x0C 复位值:0x0000 0000 该寄存器只能按字(32位)访问

名称 说明
16 BUSY 闪存忙标志位
当闪存操作正在进行时,此位被置1。当操作结束或者出错,此位被清0

7.2.6 系统复位

/* generate a systemreset to reload option byte */
NVIC_SystemReset();

其中:

// 该功能启动了一个系统复位请求,以重置MCU
__STATIC_INLINE void NVIC_SystemReset(void)
{

  // 确保在复位前完成所有未完成的内存访问,包括缓冲写
  __DSB(); 
  /* Keep priority group unchanged */
  SCB->AIRCR  = ((0x5FA << SCB_AIRCR_VECTKEY_Pos)      |
                 (SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) |
                 SCB_AIRCR_SYSRESETREQ_Msk);                   
  __DSB();                                                     
/* wait until reset */
  while(1);                                                    
}

8. 独立看门狗定时器(FWDGT)

看门狗定时器在内部计数值达到了预设的门限的时候,会触发一个复位(对于窗口看门狗定时器来说,会产生一个中断)

独立看门狗定时器(FWDGT)有独立时钟源(IRC32K)。因此就算是主时钟失效了,它仍然能保持工作状态。当内部向下计数器的计数值达到0,独立看门狗会产生一个复位。使能独立看门狗的寄存器写保护功能可以避免寄存器的值被意外的配置篡改。

8.1 功能说明

  • 启动计数 向控制寄存器(FWDGT_CTL)中写0xCCCC可以开启独立看门狗定时器,计数器开始向下计数。当计数器记到0x000,产生一次复位。
  • 重新计数 在任何时候向控制寄存器(FWDGT_CTL)中写0xAAAA都可以重装载计数器,重装载值来源于FWDGT_RLD寄存器。 软件可以在计数器计数值达到0x000之前可以通过重装载计数器来阻止看门狗定时器复位。
  • 上电即刻启动 如果在选项字节中打开了“硬件看门狗定时器”功能,那么在上电的时候看门狗定时器就被自动打开。为了避免复位,软件应该在计数器达到0x000之前重装载计数器。
  • 寄存器写保护
    • FWDGT_PSC寄存器和FWDGT_RLD寄存器都有写保护功能。在写数据到这些寄存器之前,需要写0x5555到控制寄存器(FWDGT_CTL)中
    • 写其他任何值到控制寄存器中将会再次启动对这些寄存器的写保护
    • 当预分频寄存器(FWDGT_PSC)或者重装载寄存器(FWDGT_RLD)更新时,FWDGT_STAT寄存器的状态位应该被置1

8.2 FWDGT 寄存器

FWDGT 基地址:0x4000 3000

寄存器 地址
控制寄存器 (FWDGT_CTL) 0x00
预分频寄存器 (FWDGT_PSC) 0x04
重装载寄存器 (FWDGT_RLD) 0x08
状态寄存器 (FWDGT_STAT) 0x0C

8.3 设置看门狗

  • fwdgt_config(0xfa0, FWDGT_PSC_DIV16)
    • 设置计数器重载值和预分频系数
    • 内部使用IRC32K时钟,预分频选择16,每秒2KHz
    • 溢出时间2秒

9. 控制器局域网络(CAN)

9.1 简介

  • CAN(Controller Area Network)总线是一种可以在无主机情况下实现微处理器或者设备之间相互通信的总线标准。
  • CAN 总线控制器可以处理总线上的数据收发并具有 28 个过滤器,过滤器用于筛选并接收用户需要的消息。
  • 用户可以通过 3 个发送邮箱将待发送数据传输至总线, 邮箱发送的顺序由发送调度器决定。
  • 通过 2 个深度为 3 的接收 FIFO 获取总线上的数据,接收 FIFO 的管理完全由硬件控制

    9.2 功能说明

9.2.1 框图

框图

9.2.2 工作模式

  • 睡眠工作模式
    • 芯片复位后, CAN总线控制器处于睡眠工作模式
    • 时钟停止工作并处于一种低功耗状态
    • CAN_CTL[SLPWMOD]
      • = 1,进入睡眠模式
        • CAN_STAT[SLPWS] = 1(硬件置1)
      • = 0,退出睡眠模式
    • CAN_CTL[AWU] = 1,
      • 当CAN检测到总线活动时,CAN总线控制器将自动退出睡眠工作模式
    • 从睡眠模式进入
      • 初始化工作模式
        • CAN_CTL[IWMOD] = 1, CAN_CTL[SLPWMOD] = 0
      • 正常工作模式
        • CAN_CTL[IWMOD] = 0, CAN_CTL[SLPWMOD] = 0
  • 初始化工作模式
    • 要配置 CAN 总线通信参数,CAN 总线控制器必须进入初始化工作模式
  • 正常工作模式
    • 配置完 CAN 总线通信参数
    • CAN_CTL[IWMOD] = 0,即可进入
    • 开始与 CAN 总线网络中的节点进行正常通信

9.2.3 通讯模式

  • 静默(Silent)通信模式
    • 可以从 CAN 总线接收数据
    • 但不向总线发送任何数据
    • CAN_BT[SCMOD]=1
  • 回环(Loopback)通信模式
    • 发送的数据可以被自己接收并存入接收 FIFO
    • 发送数据也送至 CAN 网络
    • CAN_BT[LCMOD]=1
  • 回环静默(Loopback and Silent)通信模式
    • CAN 的 RX 和 TX 引脚与 CAN 网络断开
    • CAN 总线控制器既不从CAN 网络接收数据,也不向 CAN 网络发送数据
    • 其发送的数据仅可以被自己接收
    • CAN_BT[LCMOD]=1, CAN_BT[SCMOD]=1
  • 正常(Normal)通信模式
    • 可以从 CAN 总线接收数据
    • 也可以向 CAN 总线发送数据
    • CAN_BT[LCMOD]=0, CAN_BT[SCMOD]=0

9.2.4 数据发送

发送寄存器

数据发送通过3个发送邮箱进行,可以通过寄存器 CAN_TMIx , CAN_TMPx , CAN_TMDATA0x 和 CAN_TMDATA1x 对发送邮箱进行配置

snd_reg

发送邮箱状态转换

  • empty
    • 当发送邮箱处于 empty 状态时, 才可以对邮箱进行配置
    • 数据发送完成,邮箱变为空闲
    • 可以再次交给应用程序使用
  • pending
    • CAN_TMIx[TEN]=1,向 CAN 总线控制器提交发送请求
    • 发送邮箱处于 pending 状态
  • scheduled
    • 超过 1 个邮箱处于 pending 状态时
    • 需要对多个邮箱进行调度
  • transmit
    • 调度完成后
    • 发送邮箱中的数据开始向 CAN 总线上发送

数据发送步骤

  1. 选择一个空闲发送邮箱
  2. 配置4个发送寄存器
  3. 将CAN_TMIx寄存器的TEN置1
  4. 检测发送状态和错误信息
    • CAN_TSTAT[MTF]=1 && CAN_TSTAT[MTFNERR]=1,发送成功
  • CAN_TSTAT[MTF]:发送完成标志位
  • CAN_TSTAT[MTFNERR]:发送完成无错误
  • CAN_TSTAT[MAL]:仲裁失败标志位
  • CAN_TSTAT[MTE]:发送错误

发送选项

  • CAN_TSTAT[MST]=1:中止数据发送
    • 邮箱处于pending和scheduled状态
      • 立即中止
    • transmit
      • 数据发送被成功,邮箱将转换为empty
      • 数据发送过程中出现了问题,邮箱将转换为scheduled状态
  • CAN_CTL[TFO]发送优先级
    • =1:照先来先发送(FIFO)的顺序进行
    • =0:最小标识符(Identifier)的邮箱最先发送

9.2.5 数据接收

接收寄存器

2个深度为3的FIFO接收来自CAN网络的数据

rev

  • CAN_RFIFO[0/1]配置和读取FIFO状态
  • 接收FIFO数据
    • CAN_RFIFOMI[0/1]
    • CAN_RFIFOMP[0/1]
    • CAN_RFIFOMDATA0[0/1]
    • CAN_RFIFOMDATA1[0/1]

接收 FIFO

  • 每个FIFO有3个接收邮箱,用来存储接收数据帧
  • 通过CNA_xx接收数据
  • CAN_RFIFOx[RFD]=1释放邮箱,并且等待其由硬件自动清0

接收 FIFO 状态信息

  • CAN_RFIFOx[RFL]:
    • FIFO中包含的帧数量
    • [0, 3]
  • CAN_RFIFOx[RFF]:
    • FIFO满状态标志位
    • 这时RFL为3
  • CAN_RFIFOx[RFO]:
    • FIFO溢出
    • 当FIFO已经包含了3个数据帧时,新的数据帧到来使FIFO发生溢出
    • CAN_CTL[RFOD]=1:新的数据帧将丢弃
    • CAN_CTL[RFOD]=0:新的数据帧将覆盖最后一帧数据

数据接收步骤

  1. 查看FIFO中帧的数量
  2. 通过CAN_xxx读取数据
  3. CAN_RFIFOx[RFD]=1释放邮箱,并且等待其由硬件自动清0

过滤功能

  • 待接收的数据帧会根据其标识符(Identifier)进行过滤
  • 过滤器包含28个单元
    • bank[0:27]
    • 每个bank单元有2个寄存器CAN_FxDATA0和CAN_FxDATA1
    • 过滤单元可以配置成32bit和16bit
      • 32bit的过滤单元的FxData[0]
        • SFID[0:10]
        • EFID[0:17]
        • FF
        • FT
  • 过滤的模式包括
    • 掩码模式
      • 掩码要求待过滤的数据帧的ID区域与掩码设定的对应位相同才能通过
    • 列表模式
      • 待过滤的数据帧的ID与预设的ID列表相同才能通过
  • 过滤序号
    • 过滤器包括28过滤单元
    • 每个单元都可以设置不同的过滤方式

      过滤单元 过滤器数据寄存器 过滤序号
      bank0 F0Data[0]-32bit-ID 0
        F0Data[1]-32bit-Mask  
      bank1 F0Data[0]-32bit-ID 1
        F0Data[1]-32bit-ID 2
  • 过滤器关联的FIFO
    • 28个过滤bank单元都可以关联到RxFIFO
    • 只有关联的过滤单元bank的数据才会进入RxFIFO中保存
  • 过滤器的激活
    • 应用程序要用过滤器需要通过CAN_FW激活
  • 过滤索引Filter Index
    • 过滤bank[N]会分配到一个/多个过滤序号(filter number)
    • 当过滤单元bank[N]过滤了数据帧,则这个数据帧的过滤索引就是过滤单元bank[N]分配的过滤序号M
    • 这时CAN_RFIFOMPx[FI] = M
    • 数据帧通过了RxFIFO[0]关联的Bank[10]过滤单元,Bank[10]分配了M,N两个过滤序号,那么这个数据帧的过滤索引为M或N中的一个
    • 而且CAN_RFIFOMP0[FI] = M/N

9.2.6 时间触发通信

  • CAN网络中的所有节点都按照一个预先设定的时间序列进行通信

9.2.7 通信参数

  • 自动重发禁止模式
    • CAN_CTL[ARD]=1
    • 数据只会被发送一次
      • 如果因为仲裁失败或者总线错误而导致发送失败
      • CAN总线控制器不会进行数据自动重发
  • 位时序(Bit time)
    • CAN协议采用位同步传输方式
      • 面向字节传输的位同步方式适用于接收在每个字节前都有起始位的情况
      • 信号从发送器到接收器,再回到发送器必须在一个位时间内完成
      • 为了达到同步的目的,除了相位缓冲段外,还需要一个传输延时段。
      • 在信号传输过程中,传输延时段被视为发送或接收延时
    • CAN总线控制器将位时间分为3个部分
      • 同步段SYNC_SEG
      • BS1(位段1 Bit segment 1)
        • 该段占用1到16个时间单元
        • 相当于传播时间段 + 相位缓冲段1
      • BS2(位段2 Bit segment 2)
        • 占用1到8个时间单元
        • 相当于相位缓冲段2

9.2.8 错误标志

  • CAN_ERR[TECNT]:发送错误计数值Transmit Error Counter
  • CAN_ERR[RECNT]:接收错误计数值Receive Error Counter
  • 离线恢复
    • 当TECNT大于255时, CAN总线控制器进入离线状态, CAN_ERR[BOERR]=1
    • CAN_CTL[ABOR]=1:检测到离线恢复序列后自动恢复
    • CAN_CTL[ABOR]=0:
      • CAN_CTL[IWMOD]=1,进入初始化工作模式
      • 然后进入正常工作模式
      • 在检测到离线恢复序列后恢复
    • 离线恢复序列
      • CAN_RX检测到128次连续11个位的隐性位

9.2.9 中断

  • CAN总线控制器占用4个中断向量,通过寄存器CAN_INTEN进行控制
  • 4 类中断源
    • 发送中断
      • CAN_TSTAT[MTF0]=1:发送邮箱0变为空闲
      • CAN_TSTAT[MTF1]=1:发送邮箱1变为空闲
      • CAN_TSTAT[MTF2]=1:发送邮箱2变为空闲
    • FIFO0 中断
      • CAN_INTEN[RFNEIE0]=1
      • FIFO0中包含待接收数据
      • FIFO0满
      • FIFO0溢出
    • 错误和工作模式改变中断
      • 错 误
        • CAN_STAT[ERRIF]=1 && CAN_INTEN[ERRIE]=1
      • 唤醒
        • CAN_STAT[WUIF]=1 && CAN_INTEN[WIE]=1

9.3 电路原理图

can

CAN 芯片管脚

名称 管脚
CAN0_RX PB8
CAN0_TX PB9
CPU_CAN_RS PF8
  • CPU_CAN_RS
    • 0:高速模式
    • 1:静默模式

9.4 代码分析

  • 设计需求
    • 1个主/n个从
      • 从机只接收来自主机的消息
      • 主机可以接收来自所有从机制消息
      • 解决方法
        • 主机的过滤器可以得到消息是从哪个从机发送来的
        • 从机的过滤器可以过滤掉所有非主机的消息
    • 每个CAN设备可以从背板的GPIO得知自己所在的槽位
      • 通过GPIO得到本机的槽位ID
      • 通过槽位ID组合成CAN总线的SFID
    • 主机通过net从上位机接收指令
    • 主机主动将命令发送给各从机
    • 从机回复信息给主机
    • 主机通过net将信息反馈给上位机
  • 协议设计
    • 任何设备首次插入背板之后
      • 读取本设备的槽位号
      • 主机
        • 广播调查报文
        • 从机收到调查报文
          • 反馈自己的槽位号
      • 从机
        • 通知主机自己槽位号
    • 主机每格一定时间发送调查报文,以确定设备在位情况
    • 主机收到上位机消息
      • 发送查询报文
      • 从机根据要查询的内容反馈信息
    • 从机在过温时,发送过温报警给主机

9.4.1 初始化CAN总线和过滤器

初始化CAN总线控制器
  1. 将CAN控制器设置为初始化状态
     CAN_CTL(can_periph) &= ~CAN_CTL_SLPWMOD; // 关闭睡眠
     CAN_CTL(can_periph) |= CAN_CTL_IWMOD;  // 进入初始化模式
     // IWS:初始化工作状态
     while((CAN_STAT_IWS != (CAN_STAT(can_periph) & CAN_STAT_IWS)) && (0U != timeout)){
         timeout--;
     }
    
  2. 设置位时序寄存器 bit timing
     CAN_BT(can_periph) = (BT_MODE((uint32_t)can_parameter_init->working_mode) | \
                             BT_SJW((uint32_t)can_parameter_init->resync_jump_width) | \
                             BT_BS1((uint32_t)can_parameter_init->time_segment_1) | \
                             BT_BS2((uint32_t)can_parameter_init->time_segment_2) | \
                             BT_BAUDPSC(((uint32_t)(can_parameter_init->prescaler) - 1U)));
    
    • 工作模式:CAN_NORMAL_MODE
    • 再同步补偿宽度SJW(resync_jump_width): CAN_BT_SJW_1TQ
    • BS1段:CAN_BT_BS1_5TQ
    • BS2段:CAN_BT_BS2_4TQ
    • 波特率预分频:5-1=4,1Mbps
       //关闭定时发送
       CAN_CTL(can_periph) &= ~CAN_CTL_TTC;
       // 关闭离线恢复序列后自动恢复
       CAN_CTL(can_periph) &= ~CAN_CTL_ABOR;
       // 关闭自动退出睡眠功能
       CAN_CTL(can_periph) &= ~CAN_CTL_AWU;
       // 启动自动重发
       CAN_CTL(can_periph) &= ~CAN_CTL_ARD;
       // 启用接收 FIFO 满时覆盖
       CAN_CTL(can_periph) &= ~CAN_CTL_RFOD;
       // FIFO发送顺序:标识符(Identifier)较小的帧先发送
       CAN_CTL(can_periph) &= ~CAN_CTL_TFO;
      
  3. 关闭初始化模式,进入正常模式
     CAN_CTL(can_periph) &= ~CAN_CTL_IWMOD;
     timeout = CAN_TIMEOUT;
     while((CAN_STAT_IWS == (CAN_STAT(can_periph) & CAN_STAT_IWS)) && (0U != timeout)){
         timeout--;
     }
    
初始化过滤器

从机只需要一个过滤器,主机需要多个过滤器来接收来自从机的消息

  1. 禁用过滤器锁
      CAN_FCTL(CAN0) |= CAN_FCTL_FLD;
    
  2. 关闭指定的过滤器
      // 关闭CAN0的过滤器0
      val = 1<<0;
      CAN_FW(CAN0) &= ~(uint32_t)val;
    
  3. 设置过滤器为32位
      // =1:32位,0:16位
      CAN_FSCFG(CAN0) |= (uint32_t)val;
    
  4. 设置CAN0的过滤器数据寄存器
      CAN_FDATA0(CAN0, can_filter_parameter_init->filter_number) = \
             FDATA_MASK_HIGH((can_filter_parameter_init->filter_list_high) & CAN_FILTER_MASK_16BITS) |
             FDATA_MASK_LOW((can_filter_parameter_init->filter_list_low) & CAN_FILTER_MASK_16BITS);
    

    把CAN0/1的filter0数据寄存器高低16位都写0x0000_0000

  5. 过滤器模式:0-掩码模式,1-列表模式
      CAN_FMCFG(CAN0) &= ~(uint32_t)val;
    
  6. 将过滤器关联 FIFO0
      CAN_FAFIFO(CAN0) &= ~(uint32_t)val;
    

    过滤器关联 FIFO 寄存器 (CAN_FAFIFO),所以是CAN0的过滤器关联FIFO0。GD32有两个接收FIFO,每个接收FIFO包含3个邮箱

  7. 启用过滤器,并锁定
      CAN_FW(CAN0) |= (uint32_t)val;
      CAN_FCTL(CAN0) &= ~CAN_FCTL_FLD;
    
  8. 启动CAN接收FIFO0非空中断
      can_interrupt_enable(CAN0, CAN_INTEN_RFNEIE0) {
     CAN_INTEN(can_periph) |= interrupt;
      }
    

9.4.2 初始化发送消息

transmit_message.tx_sfid = 0x321;
transmit_message.tx_efid = 0x00;
transmit_message.tx_ft = CAN_FT_DATA;
transmit_message.tx_ff = CAN_FF_STANDARD;
transmit_message.tx_dlen = 2;
transmit_message.tx_data[0] = 0xAB;
transmit_message.tx_data[1] = 0xCD;

9.4.3 发送消息

  1. 选择一个空邮箱
     if(CAN_TSTAT_TME0 == (CAN_TSTAT(can_periph)&CAN_TSTAT_TME0)){
         mailbox_number = CAN_MAILBOX0;
     }else if(CAN_TSTAT_TME1 == (CAN_TSTAT(can_periph)&CAN_TSTAT_TME1)){
         mailbox_number = CAN_MAILBOX1;
     }else if(CAN_TSTAT_TME2 == (CAN_TSTAT(can_periph)&CAN_TSTAT_TME2)){
         mailbox_number = CAN_MAILBOX2;
     }else{
         mailbox_number = CAN_NOMAILBOX;
     }
    
  2. 设置发送标识符寄存器
     // 启用发送
     CAN_TMI(can_periph, mailbox_number) &= CAN_TMI_TEN;
     // 设置发送帧为标准模式,帧类型为数据帧
     if(CAN_FF_STANDARD == transmit_message->tx_ff){
         /* set transmit mailbox standard identifier */
         CAN_TMI(can_periph, mailbox_number) |= (uint32_t)(TMI_SFID(transmit_message->tx_sfid) | \
                                                 transmit_message->tx_ft);
     }else{
         /* set transmit mailbox extended identifier */
         CAN_TMI(can_periph, mailbox_number) |= (uint32_t)(TMI_EFID(transmit_message->tx_efid) | \
                                                 transmit_message->tx_ff | \
                                                 transmit_message->tx_ft);
     }
    
  3. 设置数据长度为2
     CAN_TMP(can_periph, mailbox_number) &= ~CAN_TMP_DLENC;
     CAN_TMP(can_periph, mailbox_number) |= transmit_message->tx_dlen;
    

    TMP发送邮箱属性寄存器:数据长度,DLENC[3:0]表示帧内数据长度

  4. 设置发送数据
     CAN_TMDATA0(can_periph, mailbox_number) = TMDATA0_DB3(transmit_message->tx_data[3]) | \
                                               TMDATA0_DB2(transmit_message->tx_data[2]) | \
                                               TMDATA0_DB1(transmit_message->tx_data[1]) | \
                                               TMDATA0_DB0(transmit_message->tx_data[0]);
     CAN_TMDATA1(can_periph, mailbox_number) = TMDATA1_DB7(transmit_message->tx_data[7]) | \
                                               TMDATA1_DB6(transmit_message->tx_data[6]) | \
                                               TMDATA1_DB5(transmit_message->tx_data[5]) | \
                                               TMDATA1_DB4(transmit_message->tx_data[4]);
    
    • 将数据送至发送邮箱data0/1寄存器
    • 每个data寄存器是32位
  5. 启用发送
     CAN_TMI(can_periph, mailbox_number) |= CAN_TMI_TEN;
    

9.4.4 数据接收

  1. 获取接收数据的标识
     // 从CAN0的FIFO0的RFIFOMI接收FIFO邮箱标识寄存器中,取出BIT(2)数据帧格式frame format
     rx_ff = (uint8_t)(CAN_RFIFOMI_FF & CAN_RFIFOMI(can_periph, fifo_number));
     // 是标准ID
     if(CAN_FF_STANDARD == rx_ff){
         // 取出RFIFOMI[21:31],标准帧ID
         rx_sfid = GET_RFIFOMI_SFID(CAN_RFIFOMI(can_periph, fifo_number));
     }
    
  2. 得到帧类型frame type(数据帧、遥控帧)
     ft = (uint8_t)(CAN_RFIFOMI_FT & CAN_RFIFOMI(can_periph, fifo_number));        
    
  3. filtering index过滤索引
     rx_fi = (uint8_t)(GET_RFIFOMP_FI(CAN_RFIFOMP(can_periph, fifo_number)));
    
  4. 数据长度
     rx_dlen = (uint8_t)(GET_RFIFOMP_DLENC(CAN_RFIFOMP(can_periph, fifo_number)));
    
  5. 接收数据
     rx_data[0] = (uint8_t)(GET_RFIFOMDATA0_DB0(CAN_RFIFOMDATA0(can_periph, fifo_number)));
     rx_data[1] = (uint8_t)(GET_RFIFOMDATA0_DB1(CAN_RFIFOMDATA0(can_periph, fifo_number)));
     rx_data[2] = (uint8_t)(GET_RFIFOMDATA0_DB2(CAN_RFIFOMDATA0(can_periph, fifo_number)));
     rx_data[3] = (uint8_t)(GET_RFIFOMDATA0_DB3(CAN_RFIFOMDATA0(can_periph, fifo_number)));
     rx_data[4] = (uint8_t)(GET_RFIFOMDATA1_DB4(CAN_RFIFOMDATA1(can_periph, fifo_number)));
     rx_data[5] = (uint8_t)(GET_RFIFOMDATA1_DB5(CAN_RFIFOMDATA1(can_periph, fifo_number)));
     rx_data[6] = (uint8_t)(GET_RFIFOMDATA1_DB6(CAN_RFIFOMDATA1(can_periph, fifo_number)));
     rx_data[7] = (uint8_t)(GET_RFIFOMDATA1_DB7(CAN_RFIFOMDATA1(can_periph, fifo_number)));
    
  6. 释放FIFO,RFIFO0[5] = 1
     CAN_RFIFO0(can_periph) |= CAN_RFIFO0_RFD0;
    

9.4.4 中断处理函数

void CAN0_RX0_IRQHandler(void)
{
    can_message_receive(CAN0, CAN_FIFO0, &receive_message);
    if((0x321 == receive_message.rx_sfid) &&
		(CAN_FF_STANDARD == receive_message.rx_ff) && 
		(2 == receive_message.rx_dlen)){
        receive_flag = SET;
    }
}

9.4.5 主函数

while(1) {
	if(SET == receive_flag){
		receive_flag = RESET;
		printf("CAN0 recive data: %x,%x\n", 
			receive_message.rx_data[0], 
			receive_message.rx_data[1]);
	}
}

附录

关于率寄存器中的同步跳转宽度SJW值

CAN总线的同步共有两种方式:硬同步和重同步

  • 硬同步
    • 总线刚刚从空闲状态中走出来的时候,在帧头的位置都会进行一次同步
    • 此时所有的节点位时间重新开始,这种同步方式被称作硬同步
    • 硬同步时只是在有帧起始信号时起作用,无法确保后续一连串的位时序都是同步的
  • 重同步
    • CAN总线的一个位时间中包含两个缓冲段BS1和BS2
    • 在两个缓冲段中间的位置,即是读取总线电平的采样点位置
    • 当检测到总线上存在相位差的时候,通过延长BS1段或缩短BS2段来获得同步,这样的方式称为重新同步
    • 这两个相位缓冲段的延长时间或缩短时间上限由再同步跳转宽度(SJW)给定

sample

采样点是接收节点判断信号逻辑的位置,CAN通讯属于异步通讯,需要通过不断的重新同步才能保证收发节点的采样准确,所以SJW(同步跳转宽度)决定了接收节点是否能有比较好的兼容性

Cortex-M 和 FreeRTOS 的中断优先级

在移植LwIP是,系统在一个断言处出错:

configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue )

这是CortexM和FreeRTOS中断优先级相关的问题,这里做一个总结

Cortex M的中断优先级

  • cortexm是通过NVIC来控制中断优先级的
    • 对于优先级则使用中断优先级配置寄存器来管理
  • 这个优先级寄存器有8bit,通过__NVIC_PRIO_BITS来规定有效位
    • 一般配置成这个寄存器的高4位表示优先级
    • 这个高4位,又分成抢占组(优先组)和子优先组
    • 通过SCB->AIRCR来指定哪几位表示抢占组
      • 一般建议4位都表示抢占组,这样就有15个抢占级
      • 数值越小,抢占越优先

FreeRTOS 的中断优先级

#define configPRIO_BITS		       __NVIC_PRIO_BITS

#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY			0xf

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY	1

#define configKERNEL_INTERRUPT_PRIORITY		 
    ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY	 
    ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
  • configLIBRARY_LOWEST_INTERRUPT_PRIORITY
    • FreeRTOS 最低中断优先级
    • 用来配置FreeRTOS用到的SysTick中断和PendSV中断的优先级
  • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
    • FreeRTOS 能达到的最高优先级
    • 表示用户可以在抢占式优先级为1到15的中断里面调用FreeRTOS的API函数
  • configKERNEL_INTERRUPT_PRIORITY
    • FreeRTOS用来在xPortStartScheduler()中配置PendSV和SysTick的优先级
  • configMAX_SYSCALL_INTERRUPT_PRIORITY
    • FreeRTOS用来赋给寄存器basepri的
    • 可以实现全局的开关中断操作了

自定义段

在I2S的音频测试中,需要把948K的音频数据存储到0x800_2000的地址处,这是FLASH的地址空间

思路:在link文件中划分一个新的段,把音频数据放到这个段中

在lds文件中:

/* Specify the memory areas */
MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 256K
CCMRAM (rw)    : ORIGIN = 0x10000000, LENGTH = 64K
FLASH (rx)     : ORIGIN = 0x8000000, LENGTH = 3072K
WAVEDATA (rw)  : ORIGIN = 0x8050000, LENGTH = 1024K
}


/* Define output sections */
SECTIONS
{

  .fini_array :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(.fini_array*))
    KEEP (*(SORT(.fini_array.*)))
    PROVIDE_HIDDEN (__fini_array_end = .);
  } >FLASH

  /* used by the startup to initialize data */
  _sidata = .;  // &_sidata = 0x8000c08

  // 把变量放在这个位置
  ELF_MYDATA_ADDR = .;

  .wave_data :
  {
    . = ALIGN(4);
    WDATA_ADDR = .; // 把WDATA_ADDR放在这里
    *(.wave_data)  // 把所有.o文件中.wave_data的段集中到这里
    *(.wave_data*) 
    EWDATA_ADDR = .;
  } >WAVEDATA AT> FLASH 
}

这里

.wave_data : {
  ...
}

是把这个.o的内容都收集到.wave_data段中

.wave_data : {} > WAVEDATA

是设定收集好的wave_data的LMA地址放到WAVEDATA地址中

.wave_data : {} > WAVEDATA AT> FLASH

是设定收集好的wave_data的LMA地址放到WAVEDATA地址中,当VMA地址是跟在fini_array段后面

从实际的运行来看:

0x8050000  0037004d "乱数"
0x8050004  00040023 "乱数"

真正的wavetestdata数据却在:

0x8000c08 52494646  "RIFF"

而这个地址正好是_sidata变量的地址

通过arm-none-eabi-objdump -D F450z.elf >f450z.dis得到反汇编代码 然后在运行的代码中查找[0x805_0000, 0x8050003]的数据为0x0037004df450z.dis中找到这个数据在0x8000c08处 vscode调试显示16进制方法:temp,x显示16进制,temp,o显示8进制

从上面可以看出,.wave_data {} > WAVEDATA 说明.wave_data段的LMA加载地址是0x0805_0000AT>FLASH 在把.wave_data段的内容紧贴前面的FLASH中的段放置

现在只要把AT>FLASH去掉,那么.wave_data就放到了0x805_0000地址处了

#pragma pack(4)
const char wavetestdata[] __attribute__((section(".wave_data")))= {
    0x52, 0x49, 0x46, 0x46, 
    0xC8, 0xCF, 0x0E, 0x00, 
    0x57, 0x41, 0x56, 0x45, 
    0x66, 0x6D, 0x74, 0x20, 
    ...
}
  • 创建一个WAVEDATA内存区间,起始地址在FLASH空间的0x800_2000
  • 创建一个.wave_data
    • 把所有.o文件中的.wave_data段集中到这里
    • 这个.wave_data段放置到虚拟空间的WAVEDATA
  • 把音频数据wavetestdata设置在.wave_data

构建好g450z.elf后,可以看到[11] .wave_data段是放在0x0800_2000地址的

使用AT>FLASH是为了减小编译出来的bin文件大小(对elfhex文件影响不大)

比如没有使用AT>FLASH时,文件大小为:

ls -l F450z_build/F450z.*  
-rwxrwxr-x 1 wilson wilson 1306576 5月  21 10:30 F450z_build/F450z.bin
-rwxrwxr-x 1 wilson wilson 1120876 5月  21 10:30 F450z_build/F450z.elf
-rw-rw-r-- 1 wilson wilson 2739106 5月  21 10:30 F450z_build/F450z.hex
-rw-rw-r-- 1 wilson wilson  274025 5月  21 10:30 F450z_build/F450z.map

使用后:

ls -l F450z_build/F450z.*
-rwxrwxr-x 1 wilson wilson  973784 5月  21 10:32 F450z_build/F450z.bin
-rwxrwxr-x 1 wilson wilson 1120876 5月  21 10:32 F450z_build/F450z.elf
-rw-rw-r-- 1 wilson wilson 2739102 5月  21 10:32 F450z_build/F450z.hex
-rw-rw-r-- 1 wilson wilson  274089 5月  21 10:32 F450z_build/F450z.map

其中bin文件减少了很多体积

❯ readelf -S F450z_build/F450z.elf
共有 24 个节头,从偏移量 0x1116ac 开始:

节头:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .isr_vector       PROGBITS        08000000 010000 0001ac 00   A  0   0  1
  [ 2] .text             PROGBITS        080001ac 0101ac 000a54 00  AX  0   0  4
  [ 3] .rodata           PROGBITS        08000c00 0fefd0 000000 00  WA  0   0  1
  [ 4] .ARM.attributes   ARM_ATTRIBUTES  08000c00 0fefd0 00002e 00      0   0  1
  [ 5] .init_array       INIT_ARRAY      08000c00 010c00 000004 04  WA  0   0  4
  [ 6] .fini_array       FINI_ARRAY      08000c04 010c04 000004 04  WA  0   0  4
  [ 7] .data             PROGBITS        20000000 0feffe 000000 00  WA  0   0  1
  [ 8] .ccmram           PROGBITS        10000000 0feffe 000000 00   W  0   0  1
  [ 9] .bss              NOBITS          20000000 100000 00004c 00  WA  0   0  4
  [10] ._user_heap_stack NOBITS          2000004c 100000 000a00 00  WA  0   0  1
  [11] .wave_data        PROGBITS        08002000 012000 0ecfd0 00   A  0   0  1
  [12] .debug_info       PROGBITS        00000000 0feffe 005375 00      0   0  1
  [13] .debug_abbrev     PROGBITS        00000000 104373 0010b7 00      0   0  1
  [14] .debug_loc        PROGBITS        00000000 10542a 0027e7 00      0   0  1
  [15] .debug_aranges    PROGBITS        00000000 107c18 000528 00      0   0  8
  [16] .debug_ranges     PROGBITS        00000000 108140 000510 00      0   0  8
  [17] .debug_line       PROGBITS        00000000 108650 0034fd 00      0   0  1
  [18] .debug_str        PROGBITS        00000000 10bb4d 002dff 01  MS  0   0  1
  [19] .comment          PROGBITS        00000000 10e94c 00004d 01  MS  0   0  1
  [20] .debug_frame      PROGBITS        00000000 10e99c 000b1c 00      0   0  4
  [21] .symtab           SYMTAB          00000000 10f4b8 001330 10     22 147  4
  [22] .strtab           STRTAB          00000000 1107e8 000dc7 00      0   0  1
  [23] .shstrtab         STRTAB          00000000 1115af 0000fc 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

查看下位机地址空间中的内容:

音频数据地址:

riff

变量地址:

var

看到音频数据wavetestdata的地址确实在0x800_2000处,但是这个地方的内容却不是想要的内容,说明我们需要把音频数据下载到这个地址才行。

adr和ldr的区别

adr r0, =_start
ldr r1, =_start

反汇编的结果是:

0xd002401c sub r0, pc, #36
0xd0024020 ldr r1, [pc, #72]
  • adr是把pc - 36的地址给了r0,是相对寻址
    • 因为下载时,是下载到0xd0020010,所以这里pc = 0xd0020034(注意2级流水线)
  • ldr是把pc+72地址处的内容作为地址给r1,是绝对寻址,注意流水线使得执行0xd0024020这一步时,pc的值应该是0xd0024028,所以[pc, #72][0xd0024070]的内容,这个内容是d0024000也就是_start的地址
  • 所以adrpc = 0xd0020034,而ldr的到的是0xd0024000

JLink下载文件

J-Link Commander允许将不同类型的数据文件下载到目标系统的闪存中。

  • 将J-Link连接到PC
  • 将目标系统连接到J-Link
  • 启动J-Link Commander
  • 输入所需的设置(例如目标设备,接口设置等)
  • 输入以下命令
    • r
    • loadfile <PathToFile> [<DestAddr>]
  • J-Link Commander执行flash下载并打印成功的时间统计
JLinkExe -device GD32F450ZK -CommandFile ./command.jlink

实现下载,其中command.jlink

si 1
speed 4000
r
h
loadbin ./F450z_build/F450z.bin, 0x8000000

复杂的命令如下:

JLink.exe -device CC2538SF53 -if JTAG -speed 4000 \
-jtagconf -1,-1 -autoconnect 1 \
-CommanderScript C:\Work\JLinkCommandFile.jlink

download.sh

rm F450i.bin
cd ..
make clean
make -j8
cp F450i_build/F450i.bin build/
cd build
if [ -f "F450i.bin" ]; then
	JLinkExe -device GD32F450IK -CommandFile ./command.jlink
else
	echo
	echo "==========================="
	echo ">>>>>F450i.bin编译失败<<<<<"
	echo "==========================="
fi
si 1
speed 4000
r
h
loadbin ./F450i.bin, 0x8020000
quit

AHB 和 APB

类似PC系统里的北桥和南桥总线。南桥总线上挂接的都是鼠标、键盘这些慢速的设备,北桥上挂接显卡等高速设备。南桥频率低,北桥频率高。另外,南桥最后也要接到北桥上。

AHB,是Advanced High performance Bus的缩写,译作高级高性能总线,这是一种“系统总线”。 AHB主要用于高性能模块(如CPU、DMA和DSP等)之间的连接。AHB 系统由主模块、从模块和基础结构(Infrastructure)3部分组成,整个AHB总线上的传输都由主模块发出,由从模块负责回应。

APB,是Advanced Peripheral Bus的缩写,这是一种外围总线。 APB主要用于低带宽的周边外设之间的连接,例如UART、1284等,它的总线架构不像 AHB支持多个主模块,在APB里面唯一的主模块就是APB 桥。再往下,APB2负责AD,I/O,高级TIM,串口1;APB1负责DA,USB,SPI,I2C,CAN,串口2345,普通TIM。

系统复位和内核复位

内核是指处理器内核,系统就是包含内核和外设整个一起

内核复位:它会使Cortex-M4进行复位,而不会影响其外设,如GPIO、TIM、USART、SPI等这些寄存器的复位。

系统复位:这个复位会使整个芯片的所有电路都进行复位,我们查看寄存器手册时,会发现某某某寄存器复位值等于多少。

  • 在CM4中,有两种方法可以执行自我复位
    • 第一种方法,是通过置位 NVIC 中应用程序中断与复位控制寄存器(AIRCR)的 VECTRESET 位(位偏移:0)
      • 这种复位的作用范围覆盖了整个CM4处理器中,除了调试逻辑之外的所有角落
      • 但是它不会影响到 CM4 处理器外部的任何电路,所以单片机上的各片上外设和其它电路都不受影响
    • 系统复位
      • 系统复位是置位同一个寄存器中的 SYSRESETREQ 位
      • 这种复位则会波及整个芯片上的电路:
        • 它会使 CM4 处理器把送往系统复位发生器的请求线置为有效。
        • 但是系统复位发生器不是CM4的一部分,而是由芯片厂商实现,因此不同的芯片对此复位的响应也不同。
        • 因此,读者需要认真参阅该芯片规格书,明白当发生片内复位时,各外设和功能模块都会回到什么样的初始状态,或者有哪些功能模块不受影响
      • 大多数情况下,复位发生器在响应 SYSRESETREQ 时,它也会同时把 CM4 处理器的系统复位信号(SYSRESETn)置为有效
      • 从 SYSRESETREQ 被置为有效,到复位发生器执行复位命令,往往会有一个延时。在此延时期间,处理器仍然可以响应中断请求。但我们的本意往往是要让此次执行到此为止,不要再做任何其它事情了。所以,最好在发出复位请求前,先把FAULTMASK置位。因此,我在提供源代码中有这么一句:__set_FAULTMASK(1)

基础boot

链接脚本

  1. 入口: Reset_Handler
  2. 指定地址分布
     MEMORY {
       FLASH (rx)    : ORIGIN = 0x8000000, LENGTH = 0x4000
       RAM (xrw)     : ORIGIN = 0x20000100, LENGTH = 0x2FF00
       CCMRAM (rw)   : ORIGIN = 0x10000000, LENGTH = 64K
     }
     __stack_size__ = DEFINED(__stack_size__) ? __stack_size__ : 0x2000;
     __ram_end__ = ORIGIN(RAM) + LENGTH(RAM) - 4;
    
  3. 定义各个段的地址分布
    • 向量表
       .isr_vector : {
         . = ALIGN(4);
         KEEP(*(.isr_vector)) /* Startup code */
         . = ALIGN(4);
       } >FLASH
      
    • 代码段 ```c .text : { . = ALIGN(4); (.text) / .text sections (code) / *(.text) /* .text* sections (code) */

    (.glue_7) / glue arm to thumb code / *(.glue_7t) / glue thumb to arm code */ *(.eh_frame)

    KEEP ((.init)) KEEP ((.fini))

    . = ALIGN(4); _etext = .; /* define a global symbols at end of code */ } >FLASH

             - 只读数据段 
     ```c
     .rodata : {
       . = ALIGN(4);
       *(.rodata)         /* .rodata sections (constants, strings, etc.) */
       *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
       . = ALIGN(4);
     } >FLASH
    
         - 静态数据段  ```c  /* used by the startup to initialize data */  _sidata = .;
    

    /* Initialized data sections goes into RAM, load LMA copy after code / .data : AT ( _sidata ) { . = ALIGN(4); _sdata = .; / create a global symbol at data start / *(.data) / .data sections / *(.data) /* .data* sections */

    . = ALIGN(4); _edata = .; /* define a global symbol at data end */ } >RAM

     说明:`.data`段通过`>RAM`放到RAM的VMA地址,但是通过`AT (_sidata)`在编译时放到`_sidata`的LMA地址处,节省程序大小
             - CCMRAM数据段
     ```c
     .ccmram : {
       . = ALIGN(4);
       _sccmram = .;       /* create a global symbol at ccmram start */
       *(.ccmram)
       *(.ccmram*)
          
       . = ALIGN(4);
       _eccmram = .;       /* create a global symbol at ccmram end */
     } >CCMRAM AT> FLASH
    

    .ccmram段通过>CCMRAM放到CCMRAM空间中,但是编译时通过AT> FLASH接到FLASH后面 - 未初始化的数据段

     . = ALIGN(4);
     .bss : {
       /* This is used by the startup in order to initialize the .bss secion */
       _sbss = .;         /* define a global symbol at bss start */
       __bss_start__ = _sbss;
       *(.bss)
       *(.bss*)
       *(COMMON)
    
       . = ALIGN(4);
       _ebss = .;         /* define a global symbol at bss end */
       __bss_end__ = _ebss;
     } >RAM
    
         - 用户的堆栈段  ```c  ._user_stack : {    . = ALIGN(8);    . = . + __stack_size__;    . = ALIGN(8);  _estack = .;  } >RAM  ```
    

start.S

定义几个变量

.word  _sidata
.word  _sdata
.word  _edata
.word  _sbss
.word  _ebss

.word伪指令是在当前的这个地址放上_sidata标号所在的地址,我们可以看到

starts

上面的汇编指令,把地址安排在了0x0800_1084开始的地方

starts

之所以地址是从0x0800_1084开始而不是开头,是因为连接脚本指定了先放向量表、再放代码段,排到start.S的时候就是这个地址了,比如Reset_Handler就是代码段的,所以它就排在前面 codes

搬运数据到RAM

CopyData:
  ldr r3, =_sidata // LMA的数据段起始地址
  ldr r3, [r3, r1]
  str r3, [r0, r1]
  adds r1, r1, #4
    
DataInit:
  ldr r0, =_sdata // VMA数据段起始地址
  ldr r3, =_edata // VMA数据段结束地址
  adds r2, r0, r1
  cmp r2, r3
  bcc CopyData

.data数据段的内容从LMA搬运到VMA

清bss段

  ldr r2, =_sbss
  b Zerobss

完成后在请.bss

配置系统时钟进入main

/* configure the clock */
  bl  SystemInit
/* start execution of the program */
  bl main

这里

void SystemInit (void) {
  /* Reset the RCC clock configuration to the default reset state ------------*/
  /* Set IRC16MEN bit */
  RCU_CTL |= RCU_CTL_IRC16MEN;

  RCU_MODIFY

  /* Reset CFG0 register */
  RCU_CFG0 = 0x00000000U;

  /* Reset HXTALEN, CKMEN and PLLEN bits */
  RCU_CTL &= ~(RCU_CTL_PLLEN | RCU_CTL_CKMEN | RCU_CTL_HXTALEN);

  /* Reset PLLCFGR register */
  RCU_PLL = 0x24003010U;

  /* Reset HSEBYP bit */
  RCU_CTL &= ~(RCU_CTL_HXTALBPS);

  /* Disable all interrupts */
  RCU_INT = 0x00000000U;
         
  /* Configure the System clock source, PLL Multiplier and Divider factors, 
     AHB/APBx prescalers and Flash settings ----------------------------------*/
  system_clock_config();
}

其中:

#define RCU_MODIFY      {volatile uint32_t i; \
                         RCU_CFG0 |= RCU_AHB_CKSYS_DIV2; \
                         for(i=0;i<50000;i++); \
                         RCU_CFG0 |= RCU_AHB_CKSYS_DIV4; \
                         for(i=0;i<50000;i++);} 


#define RCU_AHB_CKSYS_DIV2  CFG0_AHBPSC(8)
#define RCU_AHB_CKSYS_DIV4  CFG0_AHBPSC(9)
名称 说明
[7:4] AHBPSC[3:0] 0xxx:选择 CK_SYS 时钟不分频
1000:选择 CK_SYS 时钟 2 分频
1001:选择 CK_SYS 时钟 4 分频
名称 说明
24 PLLEN 0:PLL被关闭
19 CKMEN HXTAL时钟监视器使能
0:禁止高速4 ~ 32 MHz晶体振荡器(HXTAL)时钟监视器
18 HXTALBPS 高速晶体振荡器(HXTAL)时钟旁路模式使能
0:禁止HXTAL旁路模式
16 HXTALEN 高速晶体振荡器(HXTAL)使能
0:高速4 ~ 32 MHz晶体振荡器被关闭

PLL 寄存器 (RCU_PLL)

配置PLL时钟可参考下列公式:

CK_PLLVCOSRC = CK_PLLSRC / PLLPSC CK_PLLVCO = CK_PLLVCOSRC × PLLN CK_PLLP = CK_PLLVCO / PLLP CK_PLLQ = CK_PLLVCO / PLLQ

RCU_PLL = 0x24003010U

时钟分析如下:

CK_PLLVCOSRC(1M) = CK_PLLSRC(IRC16M) / PLLPSC(16) CK_PLLVCO = CK_PLLVCOSRC × PLLN = 192M CK_PLLP = CK_PLLVCO / PLLP = 96M CK_PLLQ = CK_PLLVCO / PLLQ = 48M

名称 取值 说明
27:24 PLLQ[3:0] 4 PLLQ 输出频率的分频系数
22 PLLSEL 0 PLL时钟源选择
17:16 PLLP[1:0] 0 PLLP 输出频率分频系数
14:6 PLLN[8:0] 192 PLL VCO 时钟倍频因子
5:0 PLLPSC[5:0] 0b10000 PLL VCO 源时钟分频器
  • PLLQ 输出频率的分频系数(PLL VCO 时钟做为输入)
    • 当 PLL 被关闭时由软件置位或清零。
    • 这些位域用做将 PLL VCO 时钟 (CK_PLLVCO)分频生成 PLLQ 输出时钟(CK_PLLQ)
    • CK_PLLQ 时钟可以被用作UBSFS/USBHS (48MHz)、TRNG (48MHz)或 SDIO (≤48MHz)模块的时钟源
    • RCU_PLL 寄存器的 PLLN 位域对 CK_PLLVCO 时钟进行了描述。
    • 0000:保留
    • 0001:保留
    • 0010:CK_PLLQ = CK_PLLVCO / 2.
    • 0011:CK_PLLQ = CK_PLLVCO / 3
    • 0100:CK_PLLQ = CK_PLLVCO / 4
    • 1111:CK_PLLQ = CK_PLLVCO / 15
  • PLLSEL PLL时钟源选择
    • 由软件置位或复位,控制PLL时钟源
    • 0:IRC16M时钟被选择为PLL、PLLSAI、PLLI2S时钟的时钟源
    • 1:HXTAL时钟被选择为PLL、PLLSAI、PLLI2S时钟的时钟源
  • PLLP PLLP 输出频率分频系数(PLL VCO 时钟做为输入)
    • 当 PLL 被关闭时由软件置位或清零
    • 这些位域用做将 PLL VCO 时钟(CK_PLLVCO)分频生成 PLLP 输出时钟(CK_PLLP)。
    • CK_PLLP 时钟可以被用作系统时钟(不超过 200MHz)
    • RCU_PLL 寄存器的 PLLN 位域对 CK_PLLVCO 时钟进行了描述。
      • 00 :CK_PLLP = CK_PLLVCO / 2
      • 01 :CK_PLLP = CK_PLLVCO / 4
      • 10 :CK_PLLP = CK_PLLVCO / 6
      • 11 :CK_PLLP = CK_PLLVCO / 8
  • PLL VCO时钟倍频因子
    • 当 PLL 被关闭时由软件置位或清零(仅支持全字/半字写操作)。
    • 这些位域用做将PLL VCO 源时钟(CK_PLLVCOSRC)倍频生成 PLL VCO 输出时钟(CK_PLLVCO)
    • RCU_PLL 寄存器的 PLLPSC 位域对 CK_PLLVCOSRC 时钟进行了描述
    • 注意:CK_PLLVCO 时钟频率范围必须在 100MHz 到 500MHz 之间
    • PLLN 的值必须满足:
      • 64≤PLLN≤500 (当 RCU_PLLSSCTL 寄存器的 SSCGON=0)
      • 69≤PLLN≤500 (当 RCU_PLLSSCTL 寄存器的 SSCGON=1/SS_TYPE=0)
      • 71≤PLLN≤500 (当 RCU_PLLSSCTL 寄存器的 SSCGON=1/SS_TYPE=1)
  • PLL VCO 源时钟分频器
    • 当 PLL 被关闭时由软件置位或清零
    • 这些位域用做将 PLL 源时钟(CK_PLLSRC)分频生成 PLL VCO 源时钟(CK_PLLVCOSRC)、PLLSAI VCO 源时钟 (CK_PLLSAIVCOSRC)和 PLLI2S VCO 源时钟(CK_PLLI2SVCOSRC)
    • RCU_PLL 寄存器的 PLLSEL 位对 CK_PLLSRC 时钟进行了描述
    • VCO 源时钟频率范围必须在 1MHz 到 2MHz 之间
      • 000000:保留.
      • 000001:保留
      • 000010:CK_PLLSRC / 2
      • 000011:CK_PLLSRC / 3
      • 10000:CK_PLLSRC / 16

配置系统时钟

static void system_clock_config(void) {
    system_clock_200m_12m_hxtal();
}
static void system_clock_200m_12m_hxtal(void) {
    uint32_t timeout = 0U;
    uint32_t stab_flag = 0U;
    
    /* enable HXTAL 开启外部高速晶振 */
    RCU_CTL |= RCU_CTL_HXTALEN;

    /* wait until HXTAL is stable or the startup time is longer than HXTAL_STARTUP_TIMEOUT */
    do{
        timeout++;
        stab_flag = (RCU_CTL & RCU_CTL_HXTALSTB);
    }
    while((0U == stab_flag) && (HXTAL_STARTUP_TIMEOUT != timeout));

    /* if fail */
    if(0U == (RCU_CTL & RCU_CTL_HXTALSTB)){
      while(1){
      }
    }
         
    RCU_APB1EN |= RCU_APB1EN_PMUEN;
    PMU_CTL |= PMU_CTL_LDOVS; // 0b11高压模式

    /* HXTAL is stable */
    /* AHB = SYSCLK */
    RCU_CFG0 |= RCU_AHB_CKSYS_DIV1;
    /* APB2 = AHB/2 */
    RCU_CFG0 |= RCU_APB2_CKAHB_DIV2;
    /* APB1 = AHB/4 */
    RCU_CFG0 |= RCU_APB1_CKAHB_DIV4;

    /* Configure the main PLL, PSC = 12, PLL_N = 400, PLL_P = 2, PLL_Q = 9 */ 
    RCU_PLL = (12U | (400U << 6U) | (((2U >> 1U) - 1U) << 16U) |
                   (RCU_PLLSRC_HXTAL) | (9U << 24U));
    // 选择外部HXTAL为PLL时钟源

    /* enable PLL */
    RCU_CTL |= RCU_CTL_PLLEN;

    /* wait until PLL is stable */
    while(0U == (RCU_CTL & RCU_CTL_PLLSTB)){
    }
    
    /* Enable the high-drive to extend the clock frequency to 200 Mhz */
    PMU_CTL |= PMU_CTL_HDEN;
    while(0U == (PMU_CS & PMU_CS_HDRF)) {
    }
    
    /* select the high-drive mode */
    PMU_CTL |= PMU_CTL_HDS;
    while(0U == (PMU_CS & PMU_CS_HDSRF)) {
    } 
    
    /* select PLL as system clock */
    RCU_CFG0 &= ~RCU_CFG0_SCS;
    RCU_CFG0 |= RCU_CKSYSSRC_PLLP;

    /* wait until PLL is selected as system clock */
    while(0U == (RCU_CFG0 & RCU_SCSS_PLLP)){
    }
}

这里外部高速晶振在gd32f4xx.h中定义:

#define HXTAL_VALUE    ((uint32_t)12000000)

将数据代入如下公式:

CK_PLLVCOSRC(1M) = CK_PLLSRC(12M) / PLLPSC(12) CK_PLLVCO = CK_PLLVCOSRC × PLLN(400) = 400M CK_PLLP = CK_PLLVCO / PLLP(2) = 200M CK_PLLQ = CK_PLLVCO / PLLQ(9) = 44.4M

APB1 使能寄存器 (RCU_APB1EN)

名称 说明
28 PMUEN PMU 时钟使能
0:关闭 PMU 时钟
1:开启 PMU 时钟
17 HDS 高驱动模式切换器
该位被置位后,系统进入高驱动模式
16 HDEN 高驱动模式使能
0:禁用高驱动模式
1:使能高驱动模式

控制寄存器 (PMU_CTL)

名称 说明
15:14 LDOVS[1:0] 选择LDO输出
00:保留(LDO输出低电压模式)
01:LDO输出低电压模式
10:LDO输出中电压模式
11:LDO输出高电压模式
  • 在PLL关闭时,这些位由软件配置
  • 在主PLL使能后,LDOVS设置的值生效。
  • 如果主PLL关闭,LDO输出低电压模式被选中

时钟配置寄存器 0 (RCU_CFG0)

名称 说明
3:2 SCSS[1:0] 系统时钟选择状态
01:选择 CK_HXTAL 时钟作为 CK_SYS 时钟源
1:0 SCS[1:0] 系统时钟选择
01:选择 CK_HXTAL 时钟作为 CK_SYS 时钟源

main函数

关闭所有中断

MINI_TargetResetInit(){
	cpu_irq_disable();
  /* 关闭外设中断 */
	vIntRangeDisable(WWDGT_IRQn, FPU_IRQn); 
}

初始化看门狗

void vWatchDogInit(WORD16 timeout) {
#ifdef _INWDG
    fwdgt_config(timeout, FWDGT_PSC_DIV16);
    fwdgt_enable();
#endif
#if _EXTWDG_EN > 0
    // 使用外部看门狗 PE9
    rcu_periph_clock_enable(RCU_GPIOE);
    gpio_mode_set(GPIOE, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_9);
    gpio_output_options_set(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
    gpio_bit_toggle(EXTWDG_GPIO_PORT, EXTWDG_PIN);
#endif
}

初始化串口

UART_ucInit();
UART_SendStringByPolling("\r\n\n *** BaseBoot 310 *** \r\n\n");

检查复位原因

BOOT_STATUS_FLAG = NORMAL;
VERSION_STATUS_FLAG = NORMAL;
RESET_SRC = MINI_ucRstSrcId();

通过复位源/时钟寄存器 (RCU_RSTSCK)可以得到系统复位的原因

if (RESET_SRC == FWDG) {
    if (RUNNING_AREA == BOOT_AREA) {
        BOOT_STATUS_FLAG = ABNORMAL;
    } else if (RUNNING_AREA == VERSION_AREA) {
        VERSION_STATUS_FLAG = ABNORMAL;
    }
}

如果复位是由看门狗发起,说明运行出现问题。如果运行的是boot,说明boot有问题;如果运行的是版本,则APP版本有问题

版本CRC校验

#define BOOT_BASE 0x0000C000    /* 主Dboot起始地址(80k) */
#define BOOT_BK_BASE 0x000A0000 /* 备用Dboot起始地址(128k) */
/*!< main FLASH base address          */
#define FLASH_BASE  ((uint32_t)0x08000000U)        
// 关闭看门狗
vWatchDog();

/* 向量表错误? */
if ((chk_boot_vectbl(BOOT_BASE + FLASH_BASE) == FALSE)  
    || (RESET_SRC == FWDG && BOOT_STATUS_FLAG == ABNORMAL)) /* Dboot运行异常? */
{
    boot_roll_back();      /* Dboot回退 */
    BOOT_STATUS_FLAG = RENEWED; /* 置回退完成标志 */
}

RUNNING_AREA = BOOT_AREA;

// 启动看门狗
vWatchDog();

其中chk_boot_vectbl是对boot的向量表进行crc计算,再和原来保存的crc值比较

进入DBoot

#define FLASH_BASE   ((uint32_t)0x08000000U) 
#define BOOT_BASE     0x0000C000 


jump2dboot(){
	WORD32 ulJumpAddr = 0;
	BYTE i = 0;
	cpu_irq_disable();

	ulJumpAddr = FLASH_BASE + BOOT_BASE;

	printf("\r\n\r\n**** BaseBoot Jump into boot (");
  printf(hex2string(ulJumpAddr));
	printf(") ****\r\n\r\n");
	vDelayNms(200);

	SysTick->CTRL = 0;
	for (i = 0; i < 8; i++)
	{
		NVIC->ICER[i] = 0xFFFFFFFF;
		NVIC->ICPR[i] = 0xFFFFFFFF;
	}
	cpu_irq_enable();

	ulJumpAddr = *(WORD32 *)(FLASH_BASE + BOOT_BASE + 4);

	((void (*)(void))ulJumpAddr)();
}

DBoot

链接脚本

  1. 入口: Reset_Handler
  2. 指定地址分布
     MEMORY {
     FLASH (rx)   : ORIGIN = 0x800C000, LENGTH = 0x14000
     RAM (xrw)    : ORIGIN = 0x20000100, LENGTH = 0x2FF00
     CCMRAM (rw)  : ORIGIN = 0x10000000, LENGTH = 64K
     }
     __stack_size__ = 0x8000;
     __ram_end__ = ORIGIN(RAM) + LENGTH(RAM) - 4;
    

start.S

Rest_Handler

Reset_Handler:
/*	set boot code stack*/
  ldr sp, =_estack
/* copy the data segment into ram */  
  movs r1, #0
  b DataInit

先指定栈的位置,再搬运数据

g_pfnVectors:
    .word _estack
    .word Reset_Handler  
    .word NMI_Handler    
    .word HardFault_Handler 
    .word MemManage_Handler  
    .word BusFault_Handler   
    .word UsageFault_Handler 
    .word 0                                  /* Reserved */
    .word 0                                  /* Reserved */
    .word 0                                  /* Reserved */
    .word 0                                  /* Reserved */
    .word SVC_Handler  
    .word DebugMon_Handler  
    .word 0                 
    .word PendSV_Handler   
    .word SysTick_Handler 

    .word 0       /* Version Number Define. add is 0x40 */
    .word 0       /* Reserve */
    
    .space 44, 0  /* SDR Version String Define, Total 44 bytes. add is 0x48 */
    
    .word 0xabcdef                          
    .space 8, 0
    
    .word 0       /* Boot Version file len. add is 0x80 */
    .word 101     /* Boot Inner Version Number */
    
    .word 0       /* No Useed */
    .word 0x5a01  /*Board Type Define */
    
    .space 368, 0

开辟了boot版本文件长度,版本的CRC检验值的存储地址,所以通过编译得到的bin文件还需要经过add_crc0x80处加上文件长度,在0x48处加上版本名称(长度为6个字节),在文件最后写上文件的CRC检验值。这样得到了加上特殊数据的bin文件,再经过ver_mk把文件头加在bin文件的开头,得到最终的mcu文件,mcu最前面是整个文件的crc值

版本分析

目前版本文件有:

  1. boot.bin 通过make直接编译出来的二进制文件
  2. boot_210601.bin
    • 在0x80处加入文件大小
    • 在0x48处加入版本号(长度6字节)
    • 在文件最后加入crc检验值的文件
  3. boot_210601.mcu 在bin文件前加入了版本头之后的文件

主函数

在start.S之后,就进入了主函数main

单板初始化

  1. 打开中断
  2. 启用GPIO外设时钟
  3. 初始化看门狗
  4. 根据MCU_ID0和MCU_48V_ID1清空相应的UART数据结构
  5. 初始化调试串口
  6. 初始化I2C
  7. 获取单板硬件信息
  8. 读MMCID (因各单板控制管脚不同,会有错误)
  9. 获取本板GA地址
  10. 通用定时器初始化
  11. 面板灯初始化
  12. 打印单板复位原因
  13. 显示操作命令

IOT的main函数:

  1. 初始化使用到的GPIO管脚,包括
    • 启用GPIOA/B/C..时钟
    • JTAG管脚
    • RMII管脚
    • I2C0管脚
    • LED管脚
    • 等等
  2. 配置时钟
  3. 初始化调试串口
  4. 点亮LED
  5. 单板初始化
    • 看门狗初始化
    • EEPROM初始化
    • 从EEPROM获取单板序列号
    • TCP初始化
    • tftp客户端初始化
  6. 创建任务
    • boot 菜单:EEPROM test/ Watchdog Test/ Run App/ Earse App/ Boot Version
    • 看门狗任务
    • tftp任务,通过EEPROM的标志位判断是否需要更新App

boot进入app

boot版本

链接脚本

/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = 0x20030000;    /* end of 256K RAM */

/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 3072K
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 256K
CCMRAM (rw)      : ORIGIN = 0x10000000, LENGTH = 64K
}
  • 在链接脚本中指定地址空间的分配

start.S

g_pfnVectors:
      .word _estack
      .word Reset_Handler                      /* Vector Number 1,Reset Handler */
      .word NMI_Handler                        /* Vector Number 2,NMI Handler */
      .word HardFault_Handler                  /* Vector Number 3,Hard Fault Handler */

在start.S中确定MSP主栈指针的位置

main.c

void jump2app(void* args) {
    int i = 0;
    nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x20000);
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
    SCB->ICSR &= ~SCB_ICSR_PENDSVSET_Msk;

    cpu_irq_disable();
    for (i = 0; i < 8; i++) {
        NVIC->ICER[i] = 0xFFFFFFFF;
        NVIC->ICPR[i] = 0xFFFFFFFF;
    }

    /* jump to user application */
    jumpaddress = *(__IO uint32_t*) (APP_USED_FLASH_START_ADDRESS + 4);
    jump_to_application = (p_function)jumpaddress;

    /* initialize user application's stack pointer */
    __set_MSP(*(__IO uint32_t*) APP_USED_FLASH_START_ADDRESS);
    __set_PSP(*(__IO uint32_t*) APP_USED_FLASH_START_ADDRESS);
    jump_to_application();
}
  • 在boot的跳转函数中
    • 设定好app的中断向量表的地址
    • 关闭SysTick中断
    • 清除中断控制与状态寄存器
    • 关闭所有中断
    • 设置好MSP和PSP
    • 跳转

app版本

链接脚本

/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = 0x20030000;    /* end of 256K RAM */

/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x8020000, LENGTH = 0x40000
RAM (xrw)      : ORIGIN = 0x20000100, LENGTH = 0x2FF00
CCMRAM (rw)      : ORIGIN = 0x10000000, LENGTH = 64K
}
__stack_size__ = DEFINED(__stack_size__) ? __stack_size__ : 0x8000;
__ram_end__ = ORIGIN(RAM) + LENGTH(RAM) - 4;
  • 设置好app.bin的起始地址0x802_0000
  • 把app.bin下载到0x802_0000地址

main.c

不用特殊设置,FreeRTOS会自动启用中断

CAN 通信原理

CAN总线架构

CAN

  • CAN收发器(示意图中的单元)根据两总线CAN_H和CAN_L的电位差来判断总线电平
  • 数据传递终端的电阻器,是为了避免数据传输反射回来,使数据遭到破坏
  • 电阻阻值为120Ω

CAN通信单元的组成

  • 每个通信单元软件部分由数据帧、遥控帧、错误帧、过载帧、帧间隔组成
  • 数据帧

    数据帧

    在CAN总线上发送的数据帧不仅仅包含数据段,还有其他内容

    • 每组报文开头的11位字符为标识符,定义了报文的优先级
      • 当几个站同时竞争总线读取时,这种配置十分重要
      • 当一个站要向其它站发送数据时,该站的CPU将要发送的数据和自己的标识符传送给本站的CAN芯片,并处于准备状态;当它收到总线分配时,转为发送报文状态
  • CAN 总线的数据通信没有主从之分
    • 任意一个节点可以向任何其他(一个或多个)节点发起数据通信
    • 靠各个节点信息优先级先后顺序来决定通信次序

CAN总线非破坏性仲裁

  • 总线上每一条报文都有唯一的11位或29位数字ID
  • 当节点同时发送报文时CAN总线将按线与机制对ID的每一位进行判断
  • 当有一个节点发送0则总线的状态就是0
    • 所以ID的值越小优先级就越高
    • 这也是为什么在整车上越重要的报文ID值越小
  • 其它仲裁失败的节点将退出发送状态而转为接收节点

仲裁

节点3的ID值最小,最终取得了CAN总线的控制权

CAN总线位填充机制

  • CAN总线采用位填充技术减少消息帧在传送过程中出错
    • CAN总线规定信号的跳变沿即为同步信号,所以只要信号发生变化了,节点时钟就会被同步
    • 有连续相同的信号发送,没有跳变发生,则有可能出现发送接收节点不同步导致信号异常
    • 所以当检测到5个连续相同的位信号时,实际发送会自动插入一个补码发送,然后再接着发送原有信号。

位填充

CAN总线位时序及同步

  • 位定时是指CAN总线上一个数据位的持续时间
    • 主要用于CAN总线上各节点的通信波特率设置
    • 同一总线上的通信波特率必须相同
  • 正常的位时间=1/波特率
  • 通信双方通过软件设置相同的波特率、相同的相位调整段长度、相同的同步跳转宽度,通过以上3个元素设置,定义了CAN总线传输过程中的位时间长度以及采样点位置

CAN消息帧

  • 发动机控制单元EMS的一条报文

    EMS

    • 这条报文的名称为EMS_Control
    • ID为0x100
    • 报文发送周期是10ms
    • 报文长度为8个byte
  • CAN消息帧格式

    • 数据帧:用于传送数据

      数据帧

      • 帧起始:标志一个数据帧的开始
      • 仲裁字段
        • 在标准帧中,仲裁字段由11位ID标识符RTR位组成
          • ID(标识符)用来确定一条报文
            • 表明报文含义及优先级,如上面举例的0x100
          • RTR(远程传送请求位)
        • 在扩展帧中,仲裁字段由29位标识符、SRR位、IDE位和RTR位组成
          • IDE(标识符扩展位)
          • SRR(远程代替请求位)
      • 控制字段(标准格式)
        • IDE = 0(此时为显性电平0,表示非扩展帧)
        • 保留位r
        • DLC: 4个bit的数据字段长度码
      • 数据字段
        • CAN数据帧发送的数据,0~8个byte
      • CRC字段
      • 应答字段(包括2位)
        • 应答间隙(ACK)
        • 应答界定符(DEL)
        • 说明
          • 发送节点发出的报文中ACK及DEL均为隐性电平1
          • 接收节点正确接收后会用显性电平0覆盖隐性电平,以表示正确接收
            • 正确接收ACK=0,DEL=1;
            • 未正确接收ACK=DEL=1
      • 帧结束:7个连续的隐性位,表示数据帧结束
        • 节点在检测到11个连续的隐性位后认为总线空闲
    • 远程帧
      • 向其他节点请求发送具有同一标识符的数据帧

        远程帧

    • 错误帧
      • 当节点监测到一个或多个由CAN标准所定义的错误时,就会产生一个错误帧

        错误帧

      • 节点的错误状态有三种 错误状态
        • 主动错误状态
          • 0<REC<=127且0<TEC<=127时,节点处于主动错误状态
            • 在该状态下,节点检测到一个错误就会发送带有主动错误标志的错误帧
            • 因为主动错误标志是连续六个显性位,所以这个时候主动错误标志将会覆盖总线上其他位信号
            • 发送错误计数器TEC
            • 接收错误计数器REC
          • 节点处于主动错误状态可以正常通信
          • 处于主动错误状态的节点(可能是接收节点也可能是发送节点)在检测出错误时,发出主动错误标志
        • 被动错误状态
          • 如果某个节点发送错误帧次数较多
            • 以至于REC>127或TEC>127
            • 那么该节点就处于被动错误状态
          • 节点处于被动错误状态可以正常通信
        • 总线关闭状态
          • 已经处于被动错误状态的节点
            • 仍然多次发送被动错误帧
            • 最终使得TEC>255
            • 这样就进入总线关闭状态
          • 在该状态下,节点无法收发报文,从总线上离线
            • 只有等到检测到128次11个连续的隐性位时
            • TEC和TEC才会重新置0
            • 节点回到主动错误状态
          • 节点处于总线关闭状态不能收发报文,只能一直等待
          • 在满足一定条件时才能再次进入到主动错误状态正常收发报文
      • 错误的侦测
        • CAN控制器不仅在上电后会一直监测总线上其它节点发送的的数据包
        • 并且在自己发送数据包得过程中也在实时监测自己发送的数据
        • 一旦检测到位错误、填充错误、CRC错误、格式错误或者应答错误
        • 该节点就会根据其所处的错误状态(错误激活状态或者错误认可状态)发送相应的错误标志
    • 超载帧 超载帧
      • 在先前和后续的数据帧(或远程帧)之间提供一附加延时

过滤器

  • GD32包含28个过滤单元
    • 每个过滤单元包含2个过滤器
    • 过滤器可以设置为
      • 掩码过滤器
        • 把接收到的RxID与上掩码
      • 验证码过滤器
        • RxID必须和验证码一致
  • 过滤单元模式
    • 列表模式(验证码模式)
      • 把这个过滤单元的过滤器都设置为验证码过滤器
      • 只有和其中一个验证码一致才能进入RxFIFO
    • 掩码模式
      • 把RxID & Mask = maskID
      • maskID == list
  • 要设置的部分
    • 过滤器模式
    • 过滤器位宽
  • ID类型
    • 标准ID
      • 对于标准的CAN ID来说,我们有一个16位的寄存器来处理他足够了
    • 扩展ID
      • 扩展CAN ID,我们就必须使用32位的寄存器来处理它
  • 32位宽掩码模式
    • CAN_FxR1寄存器
      • FilterIdHigh与FilterIdLow合在一起
      • 存放验证码
    • CAN_FxR2寄存器
      • FilterMaskIdHigh与FilterMaskIdLow合在一起
      • 存放掩码
    • 对于验证码
      • 任意一个期望通过的CAN ID都是可以设为验证码的
    • 但屏蔽码
      • 却是所有期望通过的CAN ID相互同或后的最终结果
      • 这个即是屏蔽码
  • 过滤器设置
    • 16位宽的列表模式

      static void CANFilterConfig_Scale16_IdList(void) {
        CAN_FilterConfTypeDef  sFilterConfig;
        uint32_t StdId1 =0x123;						//这里采用4个标准CAN ID作为例子
        uint32_t StdId2 =0x124;
        uint32_t StdId3 =0x125;
        uint32_t StdId4 =0x126;
            
        sFilterConfig.FilterNumber = 1;				//使用过滤器1
        sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST;		//设为列表模式
        sFilterConfig.FilterScale = CAN_FILTERSCALE_16BIT;	//位宽设置为16位
        sFilterConfig.FilterIdHigh = StdId1<<5;	 //4个标准CAN ID分别放入到4个存储中
        sFilterConfig.FilterIdLow = StdId2<<5;
        sFilterConfig.FilterMaskIdHigh = StdId3<<5;
        sFilterConfig.FilterMaskIdLow = StdId4<<5;
        sFilterConfig.FilterFIFOAssignment = 0;			//接收到的报文放入到FIFO0中
        sFilterConfig.FilterActivation = ENABLE;
        sFilterConfig.BankNumber = 14;
            
        if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK) {
          Error_Handler();
        }
      }
      
    • 16位掩码模式

      void CAN_RxFilerconfig(u8 FilterNum,u8 FilterMode) {
        CAN_FilterInitTypeDef     CAN_FilterInitStructure;
        CAN_FilterInitStructure.CAN_FilterNumber=FilterNum;                 //过滤器号0~13可选
        if(FilterMode==CANRX32IDMASK) {
          CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;     //标识符屏蔽模式
          CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit;    //32位宽 
        }
        else if(FilterMode==CANRX32IDLIST)
        {
          CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdList;     //标识符列表模式
          CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit;    //32位宽 
        }
        else if(FilterMode==CANRX16IDMASK)
        {
          CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;     //标识符屏蔽模式
          CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_16bit;    //16位宽 
        }
        else if(FilterMode==CANRX16IDLIST)
        {//标识符列表模式
          CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdList;     
        //16位宽
          CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_16bit;    
        }
        //标识符寄存器FxR1
        CAN_FilterInitStructure.CAN_FilterIdHigh=0xfe1f;      //32位ID,高16位
        CAN_FilterInitStructure.CAN_FilterIdLow=0x010<<5;     //低16位
        //屏蔽寄存器FxR2
        CAN_FilterInitStructure.CAN_FilterMaskIdHigh=0xfe1f;  //32位MASK,高16位
        CAN_FilterInitStructure.CAN_FilterMaskIdLow=0x000<<5; //低16位
        //过滤器0关联到FIFO0
        CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0;  
        CAN_FilterInitStructure.CAN_FilterActivation=ENABLE;  //激活过滤器0
        //滤波器初始化
        CAN_FilterInit(&CAN_FilterInitStructure);             
      } 
      
    • 只针对标准CAN ID

      static void CANFilterConfig_Scale32_IdMask_StandardIdOnly(void) {
        CAN_FilterConfTypeDef  sFilterConfig;
        uint16_t StdIdArray[10] ={0x7e0,0x7e1,0x7e2,0x7e3,0x7e4,
                                      0x7e5,0x7e6,0x7e7,0x7e8,0x7e9}; //定义一组标准CAN ID
        uint16_t      mask,num,tmp,i;
            
        sFilterConfig.FilterNumber = 2;				//使用过滤器2
        sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;		//配置为掩码模式
        sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;	//设置为32位宽
        sFilterConfig.FilterIdHigh =(StdIdArray[0]<<5);		//验证码可以设置为StdIdArray[]数组中任意一个,这里使用StdIdArray[0]作为验证码
        sFilterConfig.FilterIdLow =0;
            
        mask =0x7ff;						//下面开始计算屏蔽码
        num =sizeof(StdIdArray)/sizeof(StdIdArray[0]);
        for(i =0; i<num; i++)		//屏蔽码位StdIdArray[]数组中所有成员的同或结果
        {
          tmp =StdIdArray[i] ^ (~StdIdArray[0]);	//所有数组成员与第0个成员进行同或操作
          mask &=tmp;
        }
        sFilterConfig.FilterMaskIdHigh =(mask<<5);
        sFilterConfig.FilterMaskIdLow =0|0x02; 		//只接收数据帧
            
        sFilterConfig.FilterFIFOAssignment = 0;		//设置通过的数据帧进入到FIFO0中
        sFilterConfig.FilterActivation = ENABLE;
        sFilterConfig.BankNumber = 14;
            
        if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
        {
          Error_Handler();
        }
      }
      
      • 过滤器2只能通过定义的标准ID StdIdArray[0]
    • 只针对扩展CAN ID

      static void CANFilterConfig_Scale32_IdMask_ExtendIdOnly(void) {
        CAN_FilterConfTypeDef  sFilterConfig;
        //定义一组扩展CAN ID用来测试
      uint32_t ExtIdArray[10] ={0x1839f101, 0x1835f102,0x1835f113,0x1835f124,
                                0x1835f105, 0x1835f106,0x1835f107,0x1835f108,
                                0x1835f109,0x1835f10A};
        uint32_t      mask,num,tmp,i;
            
        sFilterConfig.FilterNumber = 3;					//使用过滤器3
        sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;			//配置为掩码模式
        sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;		//设为32位宽
        //数组任意一个成员都可以作为验证码
        sFilterConfig.FilterIdHigh =((ExtIdArray[0]<<3) >>16) &0xffff;
        sFilterConfig.FilterIdLow =((ExtIdArray[0]<<3)&0xffff) | CAN_ID_EXT;
            
        mask =0x1fffffff;
        num =sizeof(ExtIdArray)/sizeof(ExtIdArray[0]);
        for(i =0; i<num; i++)				//屏蔽码位数组各成员相互同或的结果
        {
          tmp =ExtIdArray[i] ^ (~ExtIdArray[0]);	//都与第一个数据成员进行同或操作
          mask &=tmp;
        }
        mask <<=3;    								//对齐寄存器
        sFilterConfig.FilterMaskIdHigh = (mask>>16)&0xffff;
        sFilterConfig.FilterMaskIdLow = (mask&0xffff)|0x02; 		//只接收数据帧
        sFilterConfig.FilterFIFOAssignment = 0;
        sFilterConfig.FilterActivation = ENABLE;
        sFilterConfig.BankNumber = 14;
            
        if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
        {
          Error_Handler();
        }
      }
      
    • 标准CAN ID与扩展CAN ID混合过滤

      static void CANFilterConfig_Scale32_IdMask_StandardId_ExtendId_Mix(void) {
        CAN_FilterConfTypeDef  sFilterConfig;
        //定义一组标准CAN ID
      uint32_t StdIdArray[10] ={0x711,0x712,0x713,0x714,0x715,
                                0x716,0x717,0x718,0x719,0x71a};
        //定义另外一组扩展CAN ID
      uint32_t ExtIdArray[10] ={0x1900fAB1, 0x1900fAB2,0x1900fAB3,0x1900fAB4,
                                0x1900fAB5, 0x1900fAB6,0x1900fAB7,0x1900fAB8,
                                0x1900fAB9,0x1900fABA};
        uint32_t      mask,num,tmp,i,standard_mask,extend_mask,mix_mask;
            
        sFilterConfig.FilterNumber = 4;				//使用过滤器4
        sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;		//配置为掩码模式
        sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;	//设为32位宽
        sFilterConfig.FilterIdHigh =((ExtIdArray[0]<<3) >>16) &0xffff;	//使用第一个扩展CAN  ID作为验证码
        sFilterConfig.FilterIdLow =((ExtIdArray[0]<<3)&0xffff);
            
        standard_mask =0x7ff;		//下面是计算屏蔽码
        num =sizeof(StdIdArray)/sizeof(StdIdArray[0]);
        for(i =0; i<num; i++)			//首先计算出所有标准CAN ID的屏蔽码
        {
          tmp =StdIdArray[i] ^ (~StdIdArray[0]);
          standard_mask &=tmp;
        }
            
        extend_mask =0x1fffffff;
        num =sizeof(ExtIdArray)/sizeof(ExtIdArray[0]);
        for(i =0; i<num; i++)			//接着计算出所有扩展CAN ID的屏蔽码
        {
          tmp =ExtIdArray[i] ^ (~ExtIdArray[0]);
          extend_mask &=tmp;
        }
        mix_mask =(StdIdArray[0]<<18)^ (~ExtIdArray[0]);	//再计算标准CAN ID与扩展CAN ID混合的屏蔽码
        mask =(standard_mask<<18)& extend_mask &mix_mask;	//最后计算最终的屏蔽码
        mask <<=3;    						//对齐寄存器
          
        sFilterConfig.FilterMaskIdHigh = (mask>>16)&0xffff;
        sFilterConfig.FilterMaskIdLow = (mask&0xffff);
        sFilterConfig.FilterFIFOAssignment = 0;
        sFilterConfig.FilterActivation = ENABLE;
        sFilterConfig.BankNumber = 14;
            
        if(HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
        {
          Error_Handler();
        }
      }
      
  • GD32的CAN过滤器设置

      /* 设置过滤器0:针对本机的控制与查询消息过滤器 */
      can_filter.filter_number = 0;
      can_filter.filter_mode = CAN_FILTERMODE_LIST;
      can_filter.filter_bits = CAN_FILTERBITS_16BIT;
      /* 初始化过滤单元0 - 用于接收主机的open操作指令 */
      id4filter = ((eCanOpen & 0xf) << 4) | (can->slot & 0xf);
      can_filter.filter_list_low = (id4filter << 5);
      /* 初始化过滤单元1 - 用于接收主机的close操作指令 */
      id4filter = ((eCanClose & 0xf) << 4) | (can->slot & 0xf);
      can_filter.filter_mask_low = (id4filter << 5);
      /* 初始化过滤单元2 - 用于接收主机的query操作指令 */
      id4filter = ((eCanQuery & 0xf) << 4) | (can->slot & 0xf);
      can_filter.filter_list_high = (id4filter << 5);
      /* 初始化过滤单元3 - 用于接收主机的config操作指令 */
      id4filter = ((eCanConfig & 0xf) << 4) | (can->slot & 0xf);
      can_filter.filter_mask_high = (id4filter << 5);
      /* 设置关联FIFO */
      can_filter.filter_fifo_number = CAN_FIFO0;
      can_filter.filter_enable = ENABLE;
      can->filter_init(&can_filter, can);
    
      /* 只有CAN主机才要设置过滤器1:主机接收所有从机的传感器数据 */
      if (can->ishost) {
          can_filter.filter_number = 1;
          can_filter.filter_mode = CAN_FILTERMODE_MASK;
          can_filter.filter_bits = CAN_FILTERBITS_16BIT;
          id4filter = (eCanInfo & 0xf) << 4;
          can_filter.filter_list_low = (id4filter << 5);
          id4mask = (0xf << 4);
          can_filter.filter_mask_low = (id4mask << 5);
    
          /* 设置关联FIFO */
          can_filter.filter_fifo_number = CAN_FIFO0;
          can_filter.filter_enable = ENABLE;
          can->filter_init(&can_filter, can);
      }
    
  • GD32的CAN总线接收中断

      void CAN0_RX0_IRQHandler(void) {
        portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
        can_message_receive(CAN0, CAN_FIFO0, &can_recvmsg);
        if (CAN_FF_STANDARD == can_recvmsg.rx_ff) {
            switch (can_recvmsg.rx_fi) {
            case eFilterOpen:
                xQueueSendFromISR(can_msg_open_queue, can_recvmsg.rx_data, &xHigherPriorityTaskWoken);
                break;
            case eFilterClose:
                xQueueSendFromISR(can_msg_close_queue, can_recvmsg.rx_data, &xHigherPriorityTaskWoken);
                break;
            case eFilterQuery:
                xQueueSendFromISR(can_msg_query_queue, can_recvmsg.rx_data, &xHigherPriorityTaskWoken);
                break;
            case eFilterConfig:
                xQueueSendFromISR(can_msg_config_queue, can_recvmsg.rx_data, &xHigherPriorityTaskWoken);
                break;
            case eFilterInfo:
                xQueueSendFromISR(can_msg_info_queue, can_recvmsg.rx_data, &xHigherPriorityTaskWoken);
                break;
            }
            if (xHigherPriorityTaskWoken) {
                portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
            }
        }
      }
    
  • GD32 消息处理程序

      void can_server(void* args) {
          T_CAN_OBJ* can = can_new(eCAN_CHN0);
          T_CAN_COMM msg = { 0 };
          BaseType_t xReturn = pdFALSE;
    
          while (True) {
              xReturn = xQueueReceive(can_msg_info_queue, &msg, portMAX_DELAY);
              if (xReturn == pdFALSE) {
                  printf("CAN总线数据接收出错,错误码:%#x\n", xReturn);
              }
    
              switch (msg.property.msg_type) {
                  /* 数据消息 */
              case eMsgData: can->update_devinfo(&msg, can); break;
                  /* 控制消息 */
              case eMsgCmd: can->do_cmd(&msg); break;
                  /* 未知消息 */ 
              default:
                  printf("未知的消息类型:%#x\n", msg.property.msg_type);
              }
              printf("[app] >>> ");
              fflush(stdout);
          }
      }