GD32F450z评估板的嵌入式软件开发

 

这里记录了GD32450z评估板开发环境的搭建,以及中断、串口、DMA和I2C的demo分析

这里记录了GD32450z评估板开发环境的搭建,以及中断、串口、DMA和I2C的demo分析

1. 简介

GD32450Z-EVAL 评估板使用 GD32F450ZKT6 作为主控制器,GD32F4xx系列器件是基于Arm ®Cortex ®-M4处理器的32位通用微控制器,存储器映射重要的部分如下:

地址空间 总线 地址范围 外设
SRAM AHB互联矩阵 0x2000 0000 - 0x2001 BFFF SRAM0(112KB)
    0x2001 C000 - 0x2001 FFFF SRAM1(16KB)
    0x2002 0000 - 0x2002 FFFF SRAM2(64KB)
代码 AHB互联矩阵 0x1000 0000 - 0x1000 FFFF TCMSRAM(64KB)
    0x0800 0000 - 0x082F FFFF Main Flash(3072KB)
  • 0x0800 0000: 128MB
  • 0x1000 0000: 256MB
  • 0x2000 0000: 512MB
  • 片上 SRAM 存储器
    • GD32F4xx系列微控制器含有高达256KB片上SRAM、 4KB备份SRAM和256KB附加SRAM
    • 片上SRAM可分为4块
      • SRAM0(112KB)、SRAM1(16KB)、SRAM2(64KB)和TCMSRAM(64KB)
    • SRAM0、SRAM1和SRAM2可以被所有的AHB主机访问
    • TCMSRAM(紧耦合存储器SRAM)只可被Cortex®-M4内核的数据总线访问
  • 片上 FLASH 存储器概述
    • 高达3072KB主FLASH存储器
    • 高达30KB引导装载程序(boot loader)信息块存储器
    • 高达512B OTP(一次性可编程)存储器

1.1 引导配置

GD32F4xx系列微控制器通过 BOOT0和BOOT1引脚选择引导源。上电序列或系统复位后,ARM® Cortex®-M4处理器先从0x0000_0000地址获取栈顶值,再从 0x0000_0004地址获得引导代码的基地址,然后从引导代码的基地址开始执行程序

所选引导源对应的存储空间会被映射到引导存储空间,即从0x0000_0000开始的地址空间

1.2 cortex-M4内核启动流程

1.2.1 start.s

GD32中的startup_gd32f450.s就是bootloader程序。MCU上电时,使用的就是这个文件。它在代码最开始进行定义中断向量表。下面就来分析这个文件。

指令 作用
EQU 给数字常量取一个符号名,相当于C语言中的 #define
AREA 汇编一个新的代码段或者数据段
SPACE 分配内存空间
EXPORT 声明一个全局变量,可被外部的文件使用
DCD 以字为单位分配内存,要求4字节对齐,并要求初始化这些内存
PROC 定义子程序
ALIGN 编译器对指令或者数据的存放地址进行对齐,缺省4字节对齐
; Stack_size = 0x400 (1k大小)
Stack_Size      EQU     0x00000400

                ; 汇编一个新的STACK
                ; 不初始化、可读可写、8字节对齐
                AREA    STACK, NOINIT, READWRITE, ALIGN=3
;Stack_Mem记录当前地址
;然后分配Stack_Size的内存空间
Stack_Mem       SPACE   Stack_Size
;__Inital_sp记录当前地址
__initial_sp


__Vectors   DCD   __initial_sp       ; Top of Stack
            DCD   Reset_Handler      ; Reset Handler
            DCD   NMI_Handler        ; NMI Handler
            ...

之后是复位程序:

;/* reset Handler */
Reset_Handler   PROC
                EXPORT  Reset_Handler    [WEAK]
                IMPORT  SystemInit
                IMPORT  __main
                LDR     R0, =SystemInit
                BLX     R0
                LDR     R0, =__main
                BX      R0
                ENDP
  • 调用SystemInit进行系统初始化
  • 跳转到__mian处执行并不再跳回(因为BX R0并未保存返回地址至LR)

1.2.2 SystemInit

SystemInitGD32F4xx/system_gd32f4xx.c中,主要做:

  • 设置是否开启FPU
  • 进行系统时钟初始化配置
    • 开启内部16MHz RC振荡器
    • 复位 PLL 使能, HXTAL时钟监视器使能, 高速晶体振荡器(HXTAL)使能
    • 配置PLL
    • 复位 高速晶体振荡器(HXTAL)时钟旁路模式
    • 关闭所有中断
    • 配置系统时钟
      #ifdef __SYSTEM_CLOCK_IRC16M    
      system_clock_16m_irc16m();
      #elif defined(__SYSTEM_CLOCK_HXTAL)
      system_clock_hxtal();
      
  • 设置中断向量表偏移地址

1.3 Vscode与Jlink调试

  1. 安装JLink软件,下载地址
    • 安装在/opt/SEGGER/目录下
  2. 将JLink调试器连接到USB端口
  3. 打开JLinkConfig配置JLink
    config
  4. 按照SWD(Serial Wire Debug)方式连接JLink调试器和GD32450Z评估板
    SW jtag
  5. 打开JlinkGDBServerExe
    • 设置服务器
      linkserver
    • 连接成功
      srvsuc
  6. vscode配置
    • 按F1并写下config,然后选择C/C++:编辑配置(UI):
      {
       "configurations": [
           {
               "name": "Linux",
               "includePath": [
                   "${workspaceFolder}/**"
               ],
               "defines": [],
               "compilerPath": "/KIDE/tools/arm/gcc-arm-2020/bin/arm-none-eabi-gcc",
               "cStandard": "c11",
               "cppStandard": "c++17",
               "intelliSenseMode": "linux-gcc-arm"
           }
         ],
         "version": 4
       }
      
    • 添加构建任务
      {
          // See https://go.microsoft.com/fwlink/?LinkId=733558
          // for the documentation about the tasks.json format
          "version": "2.0.0",
          "tasks": [
              {
                  "label": "baseboot",
                  "type": "shell",
                  "command": "${workspaceRoot}/basebootbuild.sh",
                  "args": [],
                  "group": "build",
                  "presentation": {
                      // Reveal the output only if unrecognized errors occur.
                      "reveal": "always"
                  },
              }
          ]
      }
      

      注意,在构建的Makefile,必需用-g -O1才能进行调试,我尝试用-g3-g -O0构建出来的elf都无法运行

    • 添加任务配置launch.json
      {
          // Use IntelliSense to learn about possible attributes.
          // Hover to view descriptions of existing attributes.
          // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
          "version": "0.2.0",
          "configurations": [
              {
                  "type": "cortex-debug",
                  "request": "launch",
                  "name": "Debug J-Link",
                  "cwd": "${workspaceRoot}",
                  "executable": "${workspaceRoot}/baseboot_M4/Debug/BASEBOOT_M4.elf",
                  "serverpath": "/opt/SEGGER/JLink/JLinkGDBServerCLExe",
                  "serverArgs": [
                      "-select USB",
                      "-endian little",
                      "-spped auto",
                      "-noir",
                      "-LocalHostOnly"
                  ],
                  "servertype": "jlink",
                  "device": "GD32F450ZK",
                  "interface": "swd",
                  "serialNumber": "", //If you have more than one J-Link probe, add the serial number here.
                  "armToolchainPath": "/KIDE/tools/arm/gcc-arm-2020/bin/",
                  //"gdbPath": "/KIDE/tools/arm/gcc-arm-2020/bin/arm-none-eabi-gdb",
                  //"runToEntryPoint": "main",
                  "toolchainPrefix": "arm-none-eabi",
                  "svdFile": "${workspaceRoot}/GD32F4xx/GD32F4xx.svd",
                  "preLaunchTask": "baseboot",
              }
          ]
      }
      
  7. 确定自己系统的gdb可用 在我的系统中,运行了gdb,出现了问题
     $ /KIDE/tools/arm/gcc-arm-2020/bin/arm-none-eabi-gdb
     /KIDE/tools/arm/gcc-arm-2020/bin/arm-none-eabi-gdb: error while loading shared libraries: libncurses.so.5: cannot open shared object file: No such file or directory
    

    执行命令解决:

     sudo apt install libncurses5
    

    再执行gdb:

     ~ » /KIDE/tools/arm/gcc-arm-2020/bin/arm-none-eabi-gdb
    
     GNU gdb (GNU Arm Embedded Toolchain 10-2020-q4-major) 10.1.90.20201028-git
     Copyright (C) 2020 Free Software Foundation, Inc.
     License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
     This is free software: you are free to change and redistribute it.
     There is NO WARRANTY, to the extent permitted by law.
     Type "show copying" and "show warranty" for details.
     This GDB was configured as "--host=x86_64-linux-gnu --target=arm-none-eabi".
     Type "show configuration" for configuration details.
     For bug reporting instructions, please see:
     <https://www.gnu.org/software/gdb/bugs/>.
     Find the GDB manual and other documentation resources online at:
         <http://www.gnu.org/software/gdb/documentation/>.
    
     For help, type "help".
     Type "apropos word" to search for commands related to "word".
     (gdb) quit
    

2. 代码分析

2.1 键盘的外部中断

2.1.1 启用LED1的GPIO时钟

led

  1. 启用LED对应的GPIO时钟
  2. 配置GPIO输出类型与速率
    • 悬浮模式,非上/下拉
    • 输出50MHz
rcu_periph_clock_enable(RCU_GPIOD);

功能:给外设寄存器RCU_GPIOD的控制位置1 原理:

#define AHB1EN_REG_OFFSET    0x30U    /*!< AHB1 enable register offset */
#define RCU_REGIDX_BIT(regidx, bitpos)    (((uint32_t)(regidx) << 6) | (uint32_t)(bitpos))
RCU_GPIOD  = RCU_REGIDX_BIT(AHB1EN_REG_OFFSET, 3U), /*!< GPIOD clock */
  • RCU_REGIDX_BIT将寄存器偏移REGDIX和控制位BIT组合成一个32位
  • rcu_periph_clock_enable(RCU_GPIOD)相当于RCU_GPIOD[3] = 1

RCU_GPIOD可以知道,这个组合值与RCU时钟有关,而且它的寄存器是AHB1EN代表AHB1使能寄存器,在手册的4.3.10节可以查到对应的第3位:

名称 说明
3 PDEN GPIO 端口 D 时钟使能, 由软件置位或复位
0:关闭 GPIO 端口 D 时钟
1:开启 GPIO 端口 D 时钟

2.1.2 配置GPIO端口

设置GPIO引脚模式

将GPIOD[4]脚设置为输出浮空模式,GD32有PA, PB, … PI共9组GPIO,每组有[0, 15]共16个引脚

gpio_mode_set(GPIOD, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_4);

其中:

#define GPIOD   (GPIO_BASE + 0x00000C00U)
#define GPIO_MODE_OUTPUT   CTL_CLTR(1)   /*!< 输出模式 */
#define GPIO_PUPD_NONE     PUD_PUPD(0)   /*!< 浮空模式,无上拉/下拉(复位值) */
#define GPIO_PIN_4         BIT(4)        /*!< GPIO pin 4 */

设置GPIO输出类型与速度

将GPIOD[4]脚设置为输出推挽模式,输出速度为50MHz

gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4);

其中:

#define GPIO_OTYPE_PP     ((uint8_t)(0x00U))   /*!< 端口输出推挽模式 */
#define GPIO_OSPEED_50MHZ  GPIO_OSPEED_LEVEL2  /*!< output max speed 50MHz */

复位GPIO

gpio_bit_reset(GPIOD, GPIO_PIN_4);

其中:

#define GPIO_PIN_4       BIT(4)      /*!< GPIO pin 4 */
#define REG32(addr)      (*(volatile uint32_t *)(uint32_t)(addr))
#define GPIO_BC(gpiox)   REG32((gpiox) + 0x28U)    /*!< GPIO bit clear register */
GPIO_BC(gpio_periph) = (uint32_t)pin;

GPIO的位清除寄存器的偏移是0x28,对应位写1,而OCTL是指端口输出控制寄存器,它的内容为:

名称 说明
15:0 OCTLy 端口输出控制位(y=0..15)
0:引脚输出低电平
1:引脚输出高电平

2.1.3 配置GPIOC和系统配置器

rcu_periph_clock_enable(RCU_GPIOC);   // 启用GPIOC
rcu_periph_clock_enable(RCU_SYSCFG);  // 启用系统配置器
// 配置GPIOC输入模式
gpio_mode_set(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_13);

其中:

RCU_SYSCFG = RCU_REGIDX_BIT(APB2EN_REG_OFFSET, 14U),  /*!< SYSCFG clock */
名称 说明
14 SYSCFGEN SYSCFG 时钟使能,0:关闭, 1:开启

2.1.4 设置按键中断和优先级

//  启用并设置外部中断 EXTI10_15,设置优先级为[组2|子0]
nvic_irq_enable(EXTI10_15_IRQn, 2U, 0U);
// 将按键EXTI线连接到按键GPIO引脚
syscfg_exti_line_config(EXTI_SOURCE_GPIOC, EXTI_SOURCE_PIN13);
// 配置按键EXTI线
exti_init(EXTI_13, EXTI_INTERRUPT, EXTI_TRIG_RISING);
exti_interrupt_flag_clear(EXTI_13);

其中:

EXTI10_15_IRQn  = 40,  /*!< EXTI[15:10] interrupts */

EXTI:外部中断

分析中断使能函数

void nvic_irq_enable(uint8_t nvic_irq, 
                     uint8_t nvic_irq_pre_priority, 
                     uint8_t nvic_irq_sub_priority)
{
  // 使用优先级组的值来获得temp_pre和temp_sub 
  switch ((SCB->AIRCR) & (uint32_t)0x700U) {
    case NVIC_PRIGROUP_PRE0_SUB4: 
        temp_pre=0U; temp_sub=0x4U; break;
    case NVIC_PRIGROUP_PRE1_SUB3:
        temp_pre=1U; temp_sub=0x3U; break;
    ...
    default:
        nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
        temp_pre=2U; temp_sub=0x2U; 
        break;

  // 得到temp_priority以填充NVIC->IP寄存器(IP: Interrupt Priority)
  // temp_priority = group_pri + sub_pri 
  NVIC->IP[nvic_irq] = (uint8_t)temp_priority;
  // 使能中断
  NVIC->ISER[nvic_irq >> 0x05U] = (uint32_t)0x01U << (nvic_irq & (uint8_t)0x1FU);
}

这里用到了CMSIS的NVIC,它的NVIC->ISER说明在《DUI0553A_cortex_m4_dgug》的4.2.2节《Interrupt Set-enable Registers》,说明如下:

名称 说明
[31:0] SETENA 中断设置启用位
写0 = 无效;写1 = 启用中断
读 0 = 中断关闭;读1 = 中断启用

分析配置外部中断函数

// 将GPIOx[n]引脚配置为exti外部中断源, line:源
void syscfg_exti_line_config(uint8_t exti_port, uint8_t exti_pin)
  • exti_port:指定EXTI中使用的GPIO端口
    • 只有一个参数可以选择
      • EXTI_SOURCE_GPIOx(x = A,B,C,D,E,F,G,H,I): EXTI GPIO port
    • exti_pin:指定EXTI源
      • 只有一个参数可以选择
        • EXTI_SOURCE_PINx(x=0...15): EXTI GPIO引脚

在GD32F450ZK中,有一个SYSCFG配置寄存器,它的SYSCFG_EXTISSn就是用来配置外部中断与GPIO的连接的 这里

syscfg_exti_line_config(EXTI_SOURCE_GPIOC, EXTI_SOURCE_PIN13);

其中:

#define EXTI_SOURCE_GPIOC   ((uint8_t)0x02U)  /*!< EXTI GPIOC configuration */
#define EXTI_SOURCE_PIN13   ((uint8_t)0x0DU)  /*!< EXTI GPIO pin13 configuration */
syscfg_exti_line_config(2, 0xd);

指定GPIOC的13管脚为外部中断源

exti_init(EXTI_13, EXTI_INTERRUPT, EXTI_TRIG_RISING);

EXTI_13对应GPIOx[13]管理 指定外部中断源13为中断模式(对应事件模式),中断方式为上升沿触发

EXTI_13源:

  • 0000:PA13引脚
  • 0001:PB13引脚
  • 0010:PC13引脚
  • 0011:PD13引脚
  • 0100:PE13引脚
  • 0101:PF13引脚
  • 0110:PG13引脚
  • 0111:PH13引脚
exti_interrupt_flag_clear(EXTI_13);

清除中断等待寄存器

2.1.5 实现中断处理

void EXTI10_15_IRQHandler(void)
{
    // 如果外部中断源是13号
    if(RESET != exti_interrupt_flag_get(EXTI_13)){
        // 端口位翻转寄存器,翻转GPIOD[4]
        gpio_bit_toggle(GPIOD, GPIO_PIN_4);
    }
    // 向对应的PD寄存器写1,以清零
    exti_interrupt_flag_clear(EXTI_13);
}

当中断触发后,

#define APB2_BUS_BASE   ((uint32_t)0x40010000U)   /*!< apb2 base address                */
#define EXTI_BASE  (APB2_BUS_BASE + 0x00003C00U)  /*!< EXTI base address */

// EXTI 寄存器基地址: 0x4001_3C00
#define EXTI        EXTI_BASE
#define EXTI_PD     REG32(EXTI + 0x14U)      /*!< pending register */

FlagStatus exti_interrupt_flag_get(exti_line_enum linex)
{
    uint32_t flag_left, flag_right;
    
    // 中断源已经触发
    flag_left = EXTI_PD & (uint32_t)linex;
    // 中断源启用
    flag_right = EXTI_INTEN & (uint32_t)linex;
    
    // 中断源已触发 && 中断源已启用 RESET = 0, SET = 1
    if((RESET != flag_left) && (RESET != flag_right)){
        return SET;
    }else{
        return RESET;
    }
}

2.1.6 GPIOC[4]原理图

pc13

pc13p

2.2 串口打印

2.2.1 初始化LED

led_init();

void led_init(void)
{
  // 将LED对应的GPIO引脚设置成输出推挽模式,频率50MHz
    gd_eval_led_init(LED1);
    gd_eval_led_init(LED2);
    gd_eval_led_init(LED3);
}

其中

void  gd_eval_led_init (led_typedef_enum lednum)
{
    /* 启用LED时钟 */
    rcu_periph_clock_enable(GPIO_CLK[lednum]);

    /* 配置LED的GPIO端口*/ 
    // 将LED的GPIO[n]引脚设置成输出浮空模式
    gpio_mode_set(GPIO_PORT[lednum], GPIO_MODE_OUTPUT, GPIO_PUPD_NONE,GPIO_PIN[lednum]);

    // 将LED对应的GPIO输出引脚设置成输出推挽模式,频率50MHz
    gpio_output_options_set(GPIO_PORT[lednum], GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN[lednum]);

    // GPIO的位清除 Bit Clear
    GPIO_BC(GPIO_PORT[lednum]) = GPIO_PIN[lednum];
}

这里

static uint32_t GPIO_PORT[LEDn] = {LED1_GPIO_PORT, LED2_GPIO_PORT,
                                   LED3_GPIO_PORT};
static uint32_t GPIO_PIN[LEDn] = {LED1_PIN, LED2_PIN, LED3_PIN};

2.2.2 配置系统滴答

systick_config();

在这个系统tick的配置中,主要作了下面事情:

  1. 配置系统tick
      SysTick_Config(SystemCoreClock / 1000U);
    

    这个函数的原型如下:

      uint32_t SysTick_Config(uint32_t ticks)
    
    • 该函数初始化系统定时器及其中断,并启动系统定时器。
    • 计数器处于自由运行模式,以产生定期中断。
    • 参数:
      • ticks:两次中断间的tick数
    • 注意:
      • 当变量__Vendor_SysTickConfig被设置为1时,那么函数SysTick_Config不被包含。
      • 在这种情况下,文件device.h必须包含一个供应商特定的实现

        系统定时器System Timer的说明在Cortex-M4内核说明书的4.4节《System timer, SysTick》中

  2. 设置中断优先级
     NVIC_SetPriority(SysTick_IRQn, 0x00U);
    
  3. 实现SysTick_Handler
     void SysTick_Handler(void) {
       delay_decrement();
     }
    

    其中

     void delay_decrement(void) {
       if (0U != delay){
           delay--;
       }
     }
    
  4. 使用定时器
     void delay_1ms(uint32_t count) {
         delay = count;
         while(0U != delay) {}
     }
    

2.2.3 配置COM口

硬件

uart0

GPIO配置

IO线 功能 时钟 备用功能 电阻 速率
GPIOA[9] Tx EVAL_COM0_GPIO_CLK EVAL_COM0_GPIO_CLK 上拉电阻 50MHz
GPIOA[10] Rx EVAL_COM0_GPIO_CLK EVAL_COM0_GPIO_CLK 上拉电阻 50MHz

UART控制器配置

/* enable USART clock */
rcu_periph_clock_enable(COM_CLK[COM_ID]);
usart_deinit(com);
usart_baudrate_set(com, 115200U);
usart_receive_config(com, USART_RECEIVE_ENABLE);
usart_transmit_config(com, USART_TRANSMIT_ENABLE);
usart_enable(com);

/* enable USART0 receive interrupt */
usart_interrupt_enable(USART0, USART_INT_RBNE);

/* enable USART0 transmit interrupt */
usart_interrupt_enable(USART0, USART_INT_TBE);

软件配置

gd_eval_com_init(EVAL_COM0);

其中:

#define EVAL_COM0   USART0

/*!< USART0 基地址 0x4001_1000 */
#define USART0  (USART_BASE+0x0000CC00U)  
/*!< USART1 基地址 0x4000_4400 */
#define USART1  USART_BASE 

/*!< USART 基地址 */
#define USART_BASE (APB1_BUS_BASE + 0x00004400U)  

/*!< apb1 基地址 */
#define APB1_BUS_BASE ((uint32_t)0x40000000U) 

可以查手册得到:

地址空间 总线 地址范围 外设
外设 APB2 0x4001_1000 - 0x4001_13FF USART0
外设 APB1 0x4000_4400 - 0x4000_47FF USART1

并且 USART0 基地址: 0x4001_1000 USART1 基地址: 0x4000_4400

配置的过程如下

启用GPIO时钟

uint32_t COM_ID = 0;
if(EVAL_COM0 == com) {
    COM_ID = 0U;
}
rcu_periph_clock_enable(EVAL_COM0_GPIO_CLK);

rcu_periph_clock_enable(EVAL_COM0_GPIO_CLK)也就是打开AHB1EN寄存器的第0位

AHB1 使能寄存器 (RCU_AHB1EN)

名称 说明
0 PAEN GPIO 端口 A 时钟使能,0=关闭时钟;1=打开时钟

这里

#define EVAL_COM0_GPIO_CLK    RCU_GPIOA

/*!< GPIOA clock */
RCU_GPIOA = RCU_REGIDX_BIT(AHB1EN_REG_OFFSET, 0U)

启用USART时钟

rcu_periph_clock_enable(COM_CLK[COM_ID]);

其中:

static rcu_periph_enum COM_CLK[COMn] = {EVAL_COM0_CLK};


#define EVAL_COM0_CLK    RCU_USART0
/*!< USART0 时钟启用 */
RCU_USART0 = RCU_REGIDX_BIT(APB2EN_REG_OFFSET, 4U)

APB2 使能寄存器 (RCU_APB2EN)

名称 说明
4 USART0EN USART0 时钟使能;0 = 关闭时钟;1 = 启用时钟

连接GPIO引脚与UART_Tx/Rx

gpio_af_set(EVAL_COM0_GPIO_PORT, EVAL_COM0_AF, COM_TX_PIN[COM_ID]);
gpio_af_set(EVAL_COM0_GPIO_PORT, EVAL_COM0_AF, COM_RX_PIN[COM_ID]);

其中

#define EVAL_COM0_GPIO_PORT     GPIOA
#define EVAL_COM0_AF            GPIO_AF_7
static uint32_t COM_TX_PIN[COMn] = {EVAL_COM0_TX_PIN};
static uint32_t COM_RX_PIN[COMn] = {EVAL_COM0_RX_PIN};
#define EVAL_COM0_TX_PIN        GPIO_PIN_9
#define EVAL_COM0_RX_PIN        GPIO_PIN_10

gpio_af_set的函数声明如下:

void gpio_af_set(uint32_t gpio_periph,
                 uint32_t alt_func_num, 
                 uint32_t pin)
  • 设置GPIO备用功能
  • gpio_periph:GPIO端口
    • 有效值:GPIOx(x = A,B,C,D,E,F,G,H,I)
  • alt_func_num:GPIO引脚的备选功能
    • GPIO_AF_7: USART0, USART1, USART2
  • pin:GPIO引脚
    • 有效值:GPIO_PIN_x(x=0..15)

配置GPIO的Tx/Rx属性

// 配置Tx属性,上拉电阻,速率50MHz
gpio_mode_set(EVAL_COM0_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP,COM_TX_PIN[COM_ID]);
gpio_output_options_set(EVAL_COM0_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,COM_TX_PIN[COM_ID]);

/* configure USART Rx as alternate function push-pull */
// 配置Rx属性,上拉电阻,速率50MHz
gpio_mode_set(EVAL_COM0_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP,COM_RX_PIN[COM_ID]);
gpio_output_options_set(EVAL_COM0_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,COM_RX_PIN[COM_ID]);

配置UART端口

usart_deinit(com);
usart_baudrate_set(com,115200U);
usart_receive_config(com, USART_RECEIVE_ENABLE);
usart_transmit_config(com, USART_TRANSMIT_ENABLE);
usart_enable(com);
  1. 复位 UART
      void usart_deinit(uint32_t usart_periph)
      {
       switch(usart_periph){
       case USART0:
           // 相当于 RCU_APB2RST[4] = 1;
           rcu_periph_reset_enable(RCU_USART0RST);
           // 相当于 RCU_APB2RST[4] = 0;
           rcu_periph_reset_disable(RCU_USART0RST);
           break;
       case USART1:
           rcu_periph_reset_enable(RCU_USART1RST);
           rcu_periph_reset_disable(RCU_USART1RST);
           break;
       }
      }
    

    其中

      void rcu_periph_reset_enable(rcu_periph_reset_enum periph_reset) {
       RCU_REG_VAL(periph_reset) |= BIT(RCU_BIT_POS(periph_reset));
      }
    
      void rcu_periph_reset_disable(rcu_periph_reset_enum periph_reset) {
       RCU_REG_VAL(periph_reset) &= ~BIT(RCU_BIT_POS(periph_reset));
      }
    
      RCU_USART0RST = RCU_REGIDX_BIT(APB2RST_REG_OFFSET, 4U)
    
    • APB2 复位寄存器 (RCU_APB2RST)
    名称 说明
    4 UART0 RST USART0 复位,0=无作用;1=复位
  2. 设置波特率
     usart_baudrate_set(com,115200U);
    
     void usart_baudrate_set(uint32_t usart_periph, uint32_t baudval) {
         switch(usart_periph){
             /* get clock frequency */
         case USART0:
             uclk=rcu_clock_freq_get(CK_APB2);
             break;
         case USART1:
             uclk=rcu_clock_freq_get(CK_APB1);
             break;
         ...
         }
            
         if(USART_CTL0(usart_periph) & USART_CTL0_OVSMOD){
           // 如果是8倍采样模式,计算intdiv, fradiv
         } else {
           // 如果是16倍采样模式
         }
         USART_BAUD(usart_periph) = ((USART_BAUD_FRADIV | USART_BAUD_INTDIV) & (intdiv | fradiv));
     }
    

    这里rcu_clock_freq_get功能如下:

      uint32_t rcu_clock_freq_get(rcu_clock_freq_enum clock)
    
         - 获取系统时钟、总线和外设时钟频率
         - `clock`参数必须为:  - CK_SYS: 系统时钟频率  - CK_AHB: AHB clock frequency  - CK_APB1: APB1 clock frequency  - CK_APB2: APB2 clock frequency
         - 内部操作  - `sws = GET_BITS(RCU_CFG0, 2, 3);`    - 得到`RCU_CFG0[3:2]`,含义为`SCSS[1:0]`系统时钟选择状态
     - 00:选择 CK_IRC16M 时钟作为 CK_SYS 时钟源
     - 01:选择 CK_HXTAL 时钟作为 CK_SYS 时钟源    - switch(sws)
     - case 0: cksys_freq = IRC16M_VALUE;  - 计算AHB时钟频率    - `ahb_freq = cksys_freq >> clk_exp;`  - 计算APB1时钟频率    - `apb1_freq = ahb_freq >> clk_exp;`    - `apb2_freq = ahb_freq >> clk_exp;`  - 根据参数`clock`返回`ck_freq`    - switch (clock)
     - case CK_SYS: ck_freq = cksys_freq;
     - case CK_AHB: ck_freq = ahb_freq;
    
  3. 接收发送配置
       usart_receive_config(com, USART_RECEIVE_ENABLE);
       usart_transmit_config(com, USART_TRANSMIT_ENABLE);
    
       /*!< enable receiver */
       #define USART_RECEIVE_ENABLE   CTL0_REN(1)
       #define CTL0_REN(regval)  (BIT(2) & ((uint32_t)(regval) << 2))
    

    本质就是令UART_CFG0[2] = 1

    名称 说明
    3 TEN 发送器使能,0=禁用;1=启用
    2 REN 接收器使能,0=禁用;1=启用
  4. 启用UART
       usart_enable(com);
    

    本质就是令UART_CFG0[UEN] = 1, UEN(UART EN): USART使能

2.2.4 配置Tamper按键

将Tamper按键连接的GPIOC[13]设置成输入引脚

gd_eval_key_init(KEY_TAMPER, KEY_MODE_GPIO);
  1. 打开按键时钟
     rcu_periph_clock_enable(KEY_CLK[key_num]);
    
  2. 打开系统配置器
     rcu_periph_clock_enable(RCU_SYSCFG);
    
  3. 配置按键引脚GPIOx[n]为输入
     gpio_mode_set(KEY_PORT[key_num], GPIO_MODE_INPUT, GPIO_PUPD_NONE,KEY_PIN[key_num]);
    

    其中按键所在的GPIO端口为:

     static uint32_t KEY_PORT[KEYn] = {WAKEUP_KEY_GPIO_PORT, 
                                       TAMPER_KEY_GPIO_PORT,
                                       USER_KEY_GPIO_PORT};
    
     #define WAKEUP_KEY_GPIO_PORT             GPIOA
     #define TAMPER_KEY_GPIO_PORT             GPIOC
     #define USER_KEY_GPIO_PORT               GPIOB
    

    按键所在GPIO端口的引脚为:

     static uint32_t KEY_PIN[KEYn] = {WAKEUP_KEY_PIN, TAMPER_KEY_PIN,USER_KEY_PIN};
    
     #define WAKEUP_KEY_PIN                   GPIO_PIN_0
     #define TAMPER_KEY_PIN                   GPIO_PIN_13
     #define USER_KEY_PIN                     GPIO_PIN_14
    
  4. 如果按键是中断模式还要做中断配置
     if (key_mode == KEY_MODE_EXTI) {
       ...
     }
    

    按键的模式包括GPIO模式和EXTI模式,我们这里用的是GPIO模式

     typedef enum {
         KEY_MODE_GPIO = 0,
         KEY_MODE_EXTI = 1
     } keymode_typedef_enum;
    

2.2.5 通过UART0发送printf的打印信息

printf("\r\n USART printf example: please press the Tamper key \r\n");

// 相当于while(0 == COM0[USART_TC]);
while(RESET == usart_flag_get(EVAL_COM0 ,USART_FLAG_TC)) {}

#define EVAL_COM0      USART0 /* uart0 基地址 */

这里EVAL_COM0USART_FLAG_TC组成了基地址和偏移组合,表达的就是COM0[TC]的值

FlagStatus usart_flag_get(uint32_t usart_periph, usart_flag_enum flag)
{
    // USART_REG_VAL()就是 usart[flag] 基地址[偏移]的结果
    if(RESET != (USART_REG_VAL(usart_periph, flag) & BIT(USART_BIT_POS(flag)))){
        return SET;  // SET = True
    }else{
        return RESET; //RESET = False
    }
}
  • 功能
    • 是从STAT0, STAT1, CHC寄存器中获取标志flag
  • 参数
    • usart_periph:uart外设
      • USARTx(x = 0, 1, 2, 5) / USARTx(x = 3, 4,6, 7)
    • flag:usart标志
      • USART_FLAG_CTS: CTS change flag
      • USART_FLAG_TBE: transmit data buffer empty
      • USART_FLAG_TC: transmission complete
      • 等等

GNU下printf的重定向

GNU下printf的重定向与Keil是不同的,Keil只需要重新实现fputc就可以了,但GNU是另一种思路

GNU C 与 KEIL C 下的标准库函数实际上都是各个不同的机构组织编写的,虽然他们符合不同时期的C标准,如C89、C99等,那也只是用户层的API相同(同时要明白他们这些标准库是属于编译器的一部分的,就储存在编译器路径下的lib文件夹中)。虽然上层被调用的标准C函数相同,但是他们各有各的实现方式,他们在底层实现是可能完全不同的样子。所以在我们更换工具链后,一定要注意自己工程中的代码不一定会适应新的工具链开发环境。

  1. 包含头文件
     #include <stdio.h>
    
  2. 设置缓存
     setvbuf(stdout, NULL, _IONBF, 0)
    
    • 设置buffer缓存为0,这样一有数据就发送,不然会等到缓存满或有回车换行符才发送
    • 或者每次在发送的内容后添加\n或者在printf后使用fflush(stdout),来立即刷新输出流
    • 否则printf不会输出任何数据,而且会被后来的正确发送的printf数据覆盖
      • printf的数据流在扫描到\n以前会被保存在缓存中,直到\n出现或是fflush(stdout)强制刷新才会输出数据
  3. 实现GNU C库下的流函数底层_write函数
     #ifdef __GNUC__
     #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
     #define GETCHAR_PROTOTYPE int __io_getchar(void)
     #else
     #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
     #define GETCHAR_PROTOTYPE int fgetc(FILE *f)
     #endif /* __GNUC__ */
    
     PUTCHAR_PROTOTYPE
     {
         if (ch == '\n') {
       		usart_data_transmit(EVAL_COM0, (uint8_t)'\r');
       		while (RESET == usart_flag_get(EVAL_COM0, USART_FLAG_TBE))
           	;
         }
    
         usart_data_transmit(EVAL_COM0, (uint8_t)ch);
         while (RESET == usart_flag_get(EVAL_COM0, USART_FLAG_TBE))
       		;
         return ch;
     }
    
  4. 这个函数与newlib/syscalss.c中的_write冲突,注释掉它即可

GCC的Spec文件

在编译的时候,链接时使用了-specs=nano.specs--specs=nosys.specs两个选项,那么它们是干什么的呢?

简单说:

  • --specs=nano.specs:使用静态库 libc_nano.a
  • --specs=nosys.specs: 使用静态库 libnosys.a

  • nona.specs-lc 替换成 -lc_nano,使有精简版的C库替代标准C库,可以减少最终程序映像的大小
  • -specs=file 的作用
    • 编译器读取标准规格文件standard specs files后处理文件
    • 以覆盖gcc驱动程序在确定将哪些开关传递给cc1,cc1plus,as,ld等时使用的默认值
    • 可以使用多个-specs = file在命令行中指定,然后按从左到右的顺序对其进行处理
  • GCC是编译器前端,叫gcc driver
    • gcc 这个前端会根据命令行的参数再调用真正的编译器(cc1,cc1plus)、汇编器(as)、链接器(ld)
    • gcc 驱动器调用 cc1、as、ld 时遵循一定的规则,这个规则就叫做 specs
    • gcc 有一套自带的 specs ,就是上面中所说的 standard specs file
      • standard specs file 可以通过命令打印
      • gcc -dumpspecs
    • -specs=file 参数指定多个 specs 文件,gcc 会按出现的次序依次处理
      • 后面的 specs 文件可以覆盖、修改、删除前面的 specs 中的规则

nano.specs文件

文件在<gcc-install-dir>/arm-none-embed/lib/

%rename link                nano_link
%rename link_gcc_c_sequence                nano_link_gcc_c_sequence
%rename cpp_unique_options              nano_cpp_unique_options
 
*cpp_unique_options:
-isystem =/include/newlib-nano %(nano_cpp_unique_options)
 
*nano_libc:
-lc_nano
 
*nano_libgloss:
%{specs=rdimon.specs:-lrdimon_nano} %{specs=nosys.specs:-lnosys}
 
*link_gcc_c_sequence:
%(nano_link_gcc_c_sequence) --start-group %G %(nano_libc) %(nano_libgloss) --end-group
 
*link:
%(nano_link) %:replace-outfile(-lc -lc_nano) %:replace-outfile(-lg -lg_nano) %:replace-outfile(-lrdimon -lrdimon_nano) %:replace-outfile(-lstdc++ -lstdc++_nano) %:replace-outfile(-lsupc++ -lsupc++_nano)
 
*lib:
%{!shared:%{g*:-lg_nano} %{!p:%{!pg:-lc_nano}}%{p:-lc_p}%{pg:-lc_p}}
  • nona.specs-lc 替换成 -lc_nano,即:使有精简版的C库替代标准C库
  • 精简的C库有些特性是被排除掉的,比如 printf* 系列函数不支持浮点数的格式化,因为做了精简,因此最终生成的程序映像要比使用标准C库要小一些

2.2.6 联动按键与LED

由原理图可知,按下为低电平

key

if(RESET == gd_eval_key_state_get(KEY_TAMPER)){
    /* 打开LED1 */
    gd_eval_led_on(LED1);
    printf("\r\n USART printf example \r\n");
    /* 等待发送完成 */
    while(RESET == usart_flag_get(EVAL_COM0, USART_FLAG_TC)) {
    }
} else {
  // 关闭LED
  gd_eval_led_off(LED1);
}

最后实验的结果就是按下Temper按键,LED1亮且串口有输出

2.3 串口中断

2.3.1 配置UART中断

启用并设置USART0外部中断号USART0_IRQn,设置优先级为[组0 子0]
nvic_irq_enable(USART0_IRQn, 0, 0);

2.3.2 配置COM口

gd_eval_com_init(EVAL_COM0);

2.3.3. 启用USART接收/发射中断

usart_interrupt_enable(USART0, USART_INT_RBNE);
usart_interrupt_enable(USART0, USART_INT_TBE);

其中RSART_INT_RBNE中包含了USART寄存器地址和控制RBNE的位域,其含义是设置USART0CTL0[5] = 1也即UART0_CTL[RBNE_IE] = 1UART0_CTL[TBE_IE] = 1

USART_INT_RBNE = USART_REGIDX_BIT(USART_CTL0_REG_OFFSET, 5U)
USART_INT_TBE  = USART_REGIDX_BIT(USART_CTL0_REG_OFFSET, 7U)
//相当于
USART_INT_RBNE = USART_CTL0_REG_OFFSET<<6|5U;

#define USART_CTL0_REG_OFFSET    0x0CU   /*!< CTL0 register offset */

控制寄存器 0 (USART_CTL0)

地址偏移:0x0C

名称 说明
7 TBEIE 发送缓冲区空中断使能
如果该位置1,USART_STAT0寄存器中TBE被置位时产生中断
0:发送缓冲区空中断禁止
1:发送缓冲区空中断使能
5 RBNEIE 读数据缓冲区非空中断和过载错误中断使能
如果该位置1,USART_STAT0寄存器中RBNE或ORERR被置位时产生中断
0:读数据缓冲区非空中断和过载错误中断禁用
1:读数据缓冲区非空中断和过载错误中断使能

函数声明:

void usart_interrupt_enable(
          uint32_t usart_periph, 
          usart_interrupt_enum interrupt) 
{
    USART_REG_VAL(usart_periph, interrupt) 
        |= BIT(USART_BIT_POS(interrupt));
}
  • usart_periph:uart外设
    • USARTx(x = 0, 1, 2, 5) / USARTx(x = 3, 4,6, 7)
  • interrupt:USART中断标志
      /* USART 中断标志 */
      typedef enum
      {
          /* interrupt in CTL0 register */
          /*!< parity error interrupt 奇偶错误 */
          USART_INT_PERR = USART_REGIDX_BIT(USART_CTL0_REG_OFFSET, 8U),      
          /*!< transmitter buffer empty interrupt 发送区空 */
          USART_INT_TBE = USART_REGIDX_BIT(USART_CTL0_REG_OFFSET, 7U),       
          /*!< read data buffer not empty interrupt and overrun error interrupt */
          USART_INT_RBNE = USART_REGIDX_BIT(USART_CTL0_REG_OFFSET, 5U),      
          ...
      }
    

    其中:

      #define USART_REGIDX_BIT(regidx, bitpos)    (((uint32_t)(regidx) << 6) | (uint32_t)(bitpos))
    

    USART_INT_TBE相当于[寄存器偏移«6 | 寄存器位域] #define USART_CTL0_REG_OFFSET 0x0CU /*!< CTL0 register offset */

  • USART_REG_VAL
      #define USART_REG_VAL(usartx, offset)   (REG32((usartx) + (((uint32_t)(offset) & 0xFFFFU) >> 6)))
    
    • REG32相当于解引用*pReg
    • 相当于USART[offset>>6]

2.3.4 检查数据收发有效性

/* wait until USART0 send the tx_buffer */
while (tx_counter < nbr_data_to_send);

/* wait until USART0 receive the rx_buffer */
while (rx_counter < nbr_data_to_read);

/* check the received data with the send ones */
transfer_status = memory_compare(
                     tx_buffer, rx_buffer, BUFFER_SIZE);

2.3.5 设置中断处理程序

USART0有关的中断由USART0_IRQHandler处理,其处理过程如下:

  1. 如果是RBNE中断
    • 从接收器中读取一个字节
    • 如果接收数据>=应该接收的数据
      • 关闭USART0的接收中断
  2. 如果是TBE中断
    • 向发射寄存器中写入一个字节
    • 如果发射的数据量>=应该发射的数据量
      • 关闭USART0的发射中断

下面是涉及到的几个函数

获取中断标志

FlagStatus usart_interrupt_flag_get(
               uint32_t usart_periph, 
               usart_interrupt_flag_enum int_flag)
  • 功能:获取UART的中断和flag标志状态
  • 参数
    • usart_periph: USARTx(x=0,1,2,5)/UARTx(x=3,4,6,7)
    • int_flag:uart的中断标志
      • USART_INT_FLAG_TBE: transmitter buffer empty interrupt and flag
      • USART_INT_FLAG_RBNE: read data buffer not empty interrupt and flag

实现:

/* 获取UARTx中断使能状态 */
intenable = (USART_REG_VAL(usart_periph, int_flag) & BIT(USART_BIT_POS(int_flag)));
/* 获取对应flag标志位状态 */
flagstatus = (USART_REG_VAL2(usart_periph, int_flag) & BIT(USART_BIT_POS2(int_flag)));

if((0U != flagstatus) && (0U != intenable)){
    return SET;
}else{
    return RESET; 
}

其中:

#define USART_REG_VAL(usartx, offset)   (REG32((usartx) + (((uint32_t)(offset) & 0xFFFFU) >> 6)))
#define USART_REG_VAL2(usartx, offset)  (REG32((usartx) + ((uint32_t)(offset) >> 22)))

USART_REG_VAL(uartx, offset)就相当于寄存器uartx[offset]的值

/* USART interrupt flags */
typedef enum
{
    /*!< transmitter buffer empty interrupt and flag */
    USART_INT_FLAG_TBE = USART_REGIDX_BIT2(USART_CTL0_REG_OFFSET, 7U, USART_STAT0_REG_OFFSET, 7U),        

    /*!< read data buffer not empty interrupt and flag */
    USART_INT_FLAG_RBNE = USART_REGIDX_BIT2(USART_CTL0_REG_OFFSET, 5U, USART_STAT0_REG_OFFSET, 5U),       
    ...
}usart_interrupt_flag_enum;

#define USART_STAT0_REG_OFFSET      0x00U    /*!< STAT0 register offset */

状态寄存器 0 (USART_STAT0)

地址偏移:0x00

名称 说明
7 TBE 发送数据缓冲区空
上电复位或待发送数据已发送至移位寄存器后,该位置1。USART_CTL0寄存器中 TBEIE被置位将产生中断。
该位在软件将待发送数据写入USART_DATA时被清0。
0:发送数据缓冲区不为空
1:发送数据缓冲区空
5 RBNE 读数据缓冲区非空
当读数据缓冲区接收到来自移位寄存器的数据时,该位置1。当寄存器USART_CTL0 的RBNEIE位被置位,将会有中断产生。
软件可以通过对该位写0或读USART_DATA寄存器来将该位清0。
0:读数据缓冲区为空
1:读数据缓冲区不为空
#define USART_REGIDX_BIT2(regidx, bitpos, regidx2, bitpos2)   
				(((uint32_t)(regidx2) << 22) | 
				(uint32_t)((bitpos2) << 16) | 
				(((uint32_t)(regidx) << 6) | 
				(uint32_t)(bitpos)))

UART接收数据函数

uint16_t usart_data_receive(uint32_t usart_periph) {
    return (uint16_t)(GET_BITS(USART_DATA(usart_periph), 0U, 8U));
}

/*!< USART data register */
#define USART_DATA(usartx)    REG32((usartx) + 0x04U)  

数据寄存器 (USART_DATA)

地址偏移:0x04

名称 说明  
[31:9] - 保留  
[8:0] DATA[8:0] 发送或接收的数据值
软件可以通过写这些位来改变要发送的数据,或读这些位的值来获取接收到的数据。
如果使能了奇偶校验,当发送数据被写入寄存器,数据的最高位(第7位或第8位取决于USART_CTL0寄存器的WL位)将被校验位取代。

关闭UART的中断

void usart_interrupt_disable(
                 uint32_t usart_periph, 
                 usart_interrupt_enum interrupt)
{
    USART_REG_VAL(usart_periph, interrupt) 
                 &= ~BIT(USART_BIT_POS(interrupt));
}

UART发送数据函数

把数据写到UART的数据寄存器中

void usart_data_transmit(uint32_t usart_periph, uint32_t data)
{
    USART_DATA(usart_periph) = ((uint16_t)USART_DATA_DATA & data);
}

/* USARTx_DATA */
/*!< transmit or read data value */
#define USART_DATA_DATA   BITS(0,8)

UART中断实现

void USART0_IRQHandler(void) {
  // 如果设置了RBNE中断,且中断标志是RBNE
  if ((RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) &&
      (RESET != usart_flag_get(USART0, USART_FLAG_RBNE)))
  {
      /* Read one byte from the receive data register */
      rx_buffer[rx_counter++] = (uint8_t)usart_data_receive(USART0);

      if (rx_counter >= nbr_data_to_read)
      {
          /* disable the USART0 receive interrupt */
          usart_interrupt_disable(USART0, USART_INT_RBNE);
      }
  }

// 如果设置了UART0的TBE中断,且中断标志是TBE
  if ((RESET != usart_flag_get(USART0, USART_FLAG_TBE)) &&
      (RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_TBE)))
  {
      /* Write one byte to the transmit data register */
      usart_data_transmit(USART0, tx_buffer[tx_counter++]);

      if (tx_counter >= nbr_data_to_send)
      {
          /* disable the USART0 transmit interrupt */
          usart_interrupt_disable(USART0, USART_INT_TBE);
      }
  }
}

最终结果

设置

uint8_t tx_buffer[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};

运行程序后串口打印:

sudo picocom -b 115200 /dev/ttyS0
[sudo] wilson 的密码: 
picocom v1.7

port is        : /dev/ttyS0
flowcontrol    : none
baudrate is    : 115200
parity is      : none
databits are   : 8
escape is      : C-a
local echo is  : no
noinit is      : no
noreset is     : no
nolock is      : no
send_cmd is    : sz -vv
receive_cmd is : rz -vv
imap is        : 
omap is        : 
emap is        : crcrlf,delbs,

Terminal ready
abcdefg

在串口中输入abcdefg后,流水灯闪烁

2.4 串口DMA

2.4.1 DMA简介

  • 直接存储器访问控制器(DMA),DMA控制器在没有MCU参与的情况下从一个地址向另一个地址传输数据
  • 每个DMA控制器包含了两个AHB总线接口和8个4字深度的FIFO
  • 每个通道可以被分配给一个或多个特定的外设进行数据传输
  • 两个内置的总线仲裁器用来处理DMA请求的优先级问题
  • 支持三种传输方式:
    • 存储器到外设
    • 外设到存储器
    • 存储器到存储器(仅 DMA1 支持)

系统结构

sys

外设握手

为了保证数据的有效传输, DMA控制器中引入了外设和存储器的握手机制,包括请求信号和应答信号:

  • 请求信号
    • 由外设发出,表明外设已经准备好发送或接收数据
  • 应答信号
    • 由 DMA 控制器响应,表明 DMA 控制器已经发送 AHB 命令去访问外设

每个 DMA 控制器有 8 个通道,每个通道有多个外设请求。寄存器 DMA_CHxCTL 的 PERIEN 位域决定了 DMA 通道选中的外设请求。

dma11 dma12

同一个外设请求可以连接到两个 DMA 通道上,这里禁止两个 DMA 通道选择相同的外设请求。例如,在 DMA0 控制器中, SPI2_RX 外设请求连接到通道 0 和通道 2。当寄存器 DMA_CH0CTL,DMA_CH2CTL 的 PERIEN 位域同时配置为‘0b000’时,使能通道 0 和通道 2,当 SPI2 发出 DMA 请求时,会造 成通道 0 和通道 2 的响应混乱,及数据传输错误

2.4.2 初始化

  1. 初始化LED
     led_init();
    
  2. 初始化systicks
     systick_config();
    
  3. 配置uart0中断
     nvic_irq_enable(USART0_IRQn, 0, 0);
    
  4. 配置COM0口
     gd_eval_com_init(EVAL_COM0);
    

2.4.3 配置UART的DMA

usart_dma_config();

开启DMA1时钟

rcu_periph_clock_enable(RCU_DMA1);

解除对DMA通道寄存器的初始化

解除对DMA1通道7寄存器的初始化

dma_deinit(DMA1, DMA_CH7);

/*!< DMA1 base address 0x4002 6400 */
#define DMA1   (DMA_BASE + 0x0400U)          
/*!< DMA base address  */
#define DMA_BASE   (AHB1_BUS_BASE + 0x00006000U)  
/*!< ahb1 base address */
#define AHB1_BUS_BASE   ((uint32_t)0x40020000U)        
DMA_CH7 = 7;

DMA 寄存器

DMA0 基地址: 0x4002 6000 DMA1 基地址: 0x4002 6400

接口说明
void dma_deinit(uint32_t dma_periph, dma_channel_enum channelx)
  • dma_periph: 指定DMA
    • DMAx(x=0,1)
    • DMAx(x=0,1)
  • channelx: 指定哪个DMA通道被取消初始化
    • DMA_CHx(x=0..7)
接口实现
  1. 关闭DMA的一个通道
     DMA_CHCTL(dma_periph,channelx) &= ~DMA_CHXCTL_CHEN;
    
     /*!< the address of DMA channel CHXCTL register  */
     #define DMA_CHCTL(dma,channel)   REG32(((dma) + 0x10U) + 0x18U*(channel))  
     /*!< channel x enable */
     #define DMA_CHXCTL_CHEN          BIT(0)                            
    

    通道 x 控制寄存器 (DMA_CHxCTL)

    地址偏移:0x10 + 0x18*x

    名称 说明
    0 CHEN 通道使能,软件置1,硬件清0
    0 : 通道禁止, 1 : 通道使能
    该位置1,DMA传输开始。发生以下情况该位会被自动清0:
    1. 数据传输完成
    2. 发生FIFO配置错误或者传输错误
    软件清0操作后,读该位仍为1代表还有正在进行的数据传输,软件查询该位可以确定DMA通道是否空闲,可以进行新的数据传输
  2. 重新设置DMA寄存器
     // 通道控制寄存器
     DMA_CHCTL(dma_periph,channelx) = DMA_CHCTL_RESET_VALUE;
     // 通道 x 计数寄存器 (DMA_CHxCNT)
     DMA_CHCNT(dma_periph,channelx) = DMA_CHCNT_RESET_VALUE;
     // 通道 x 外设基地址寄存器 (DMA_CHxPADDR)
     DMA_CHPADDR(dma_periph,channelx) = DMA_CHPADDR_RESET_VALUE;
     // 通道 x 存储器 0 基地址寄存器 (DMA_CHxM0ADDR)
     DMA_CHM0ADDR(dma_periph,channelx) = DMA_CHMADDR_RESET_VALUE;
     DMA_CHM1ADDR(dma_periph,channelx) = DMA_CHMADDR_RESET_VALUE;
     // 通道 xFIFO 控制寄存器 (DMA_CHxFCTL)
     DMA_CHFCTL(dma_periph,channelx) = DMA_CHFCTL_RESET_VALUE;
    

    以上寄存器全部置0

     // 中断标志位清除寄存器 0 (DMA_INTC0)
     if(channelx < DMA_CH4){
         DMA_INTC0(dma_periph) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE,channelx);
     }else{
         channelx -= (dma_channel_enum)4;
         DMA_INTC1(dma_periph) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE,channelx);
     }
    

    给通道x对应的中断标志位置1以清除中断标志位

  3. 填充DMA初始化数据结构
     /* DMA singledata mode initialize struct */
     typedef struct
     {
         /*!< 外设基地址 */
         uint32_t periph_addr;    = DMA_MEMORY_TO_PERIPH;
         /*!< peripheral increasing mode */  
         uint32_t periph_inc;     = DMA_PERIPH_INCREASE_DISABLE;
    
         /*!< memory 0 base address */
         uint32_t memory0_addr;   = (uint32_t)tx_buffer;
         /*!< memory increasing mode */
         uint32_t memory_inc;     = DMA_MEMORY_INCREASE_ENABLE;
    
         /*!< transfer data size of peripheral */
         uint32_t periph_memory_width;  = DMA_PERIPH_WIDTH_8BIT;
    
         /*!< DMA circular mode */
         uint32_t circular_mode; 
         /*!< channel data transfer direction */
         uint32_t direction;      = DMA_MEMORY_TO_PERIPH;
         /*!< channel transfer number */
         uint32_t number;         = ARRAYNUM(tx_buffer);
         /*!< channel priority level */
         uint32_t priority;       = DMA_PRIORITY_ULTRA_HIGH;
     } dma_single_data_parameter_struct;
    
  4. 初始化DMA1单次数据传输模式
     dma_single_data_mode_init(DMA1, DMA_CH7, &dma_init_struct);
    
    • 选择单次数据传输模式
      • DMA_CHxFCTL[2] = 0

        名称 说明
        2 MDMEN 多数据传输模式使能
        0 : 关闭多数据传输模式
        1 : 打开多数据传输模式
        CHEN为1时不可写入
        如果寄存器DMA_CHxCTL的TM位域为‘10’,在通道使能后,该位由硬件强制置1
        名称 说明
        [7:6] TM 传输方式, 软件置1与清0
        00 : 读外设写存储器
        01 : 读存储器写外设
        10 : 读存储器写存储器
        11 : 保留
        CHEN为1时不可写入
    • 外设基地址
      • DMA_CHxPADDR = USART0_DATA_ADDRESS
      • USART0_DATA_ADDRESS = ((uint32_t)0x40011004)
      • 0x40011004是UART的数据寄存器(USART_DATA)
    • 内存基地址
      • DMA_CHxM1ADDR = tx_buffer
    • 传输剩余数据量
      • DMA_CHxCNT = ARRAYNUM(tx_buffer)
    • 外设与内存传输宽度、优先级和方向
      • 外设与内存宽度一致:DMA_PERIPH_WIDTH_8BIT
      • 优先级:DMA_PRIORITY_ULTRA_HIGH (0b11 : 超高)
      • 传输方向:DMA_MEMORY_TO_PERIPH (0b01 : 读存储器写外设)
    • 设置外设增加模式
      • DMA_CHxCTL[9] = 0:固定地址模式
    • 设置内存增加模式
      • DMA_CHxCTL[10] = 1:增量地址模式
    • 设置DMA循环模式
      • DMA_CHxCTL[8] = 1: 打开循环模式
  5. 关闭DMA循环模式
     dma_circulation_disable(DMA1, DMA_CH7);
    
  6. DMA通道外设选择
     dma_channel_subperipheral_select(DMA1, DMA_CH7, DMA_SUBPERI4);
    
    • 参数
      • DMA外设DMA1
      • DMA通道CH7
      • DMA子外设4
    • 流程
      • DMA_CHxCTL[27:25] = 0x00
      • DMA_CHxCTL[27:25] = 0b100:使能外设4
  7. 设置外设到内存的DMA1通道2
     dma_deinit(DMA1, DMA_CH2);
     dma_init_struct.direction = DMA_PERIPH_TO_MEMORY;
     dma_init_struct.memory0_addr = (uint32_t)rx_buffer;
     dma_single_data_mode_init(DMA1, DMA_CH2, &dma_init_struct);
     /* configure DMA mode */
     dma_circulation_disable(DMA1, DMA_CH2);
     dma_channel_subperipheral_select(DMA1, DMA_CH2, DMA_SUBPERI4);
    

2.4.4 启用USART0 DMA通道的发送和接收

dma_channel_enable(DMA1, DMA_CH7);
dma_channel_enable(DMA1, DMA_CH2);

相当于:

DMA1_CHCTL_CH7[0] = 1; // CHEN:通道使能
DMA1_CHCTL_CH2[0] = 1; // CHEN:通道使能

2.4.5 启用发送和接收的USART DMA

usart_dma_transmit_config(USART0, USART_DENT_ENABLE);
usart_dma_receive_config(USART0, USART_DENR_ENABLE);
usart_dma_transmit_config(USART0, USART_DENT_ENABLE);
{
    ctl = USART_CTL2(usart_periph);
    ctl &= ~USART_CTL2_DENT;
    ctl |= dmacmd;  // 等于(1<<7)
    /* configure DMA transmission */
    USART_CTL2(usart_periph) = ctl;
}

其中:

/*!< USART control register 2 */
#define USART_CTL2(usartx)  REG32((usartx) + 0x14U)        

/*!< DMA request enable for transmission */
#define USART_CTL2_DENT     BIT(7)       

/*!< DMA request enable for transmission */
#define USART_DENT_ENABLE   CLT2_DENT(1)  // Bit(7)

控制寄存器 2 (USART_CTL2)

地址偏移:0x14

名称 说明
7 DENT DMA发送使能
0:DMA发送模式禁用
1:DMA发送模式使能
6 DENR DMA接收使能
0:DMA接收模式禁用
1:DMA接收模式使能

2.4.6 等待接收与发送完成

// USART0 TX DMA1通道传输完成
while(RESET == dma_flag_get(DMA1, DMA_CH7, DMA_INTF_FTFIF)) 
  ;

// USART0 RX DMA1通道传输完成
while(RESET == dma_flag_get(DMA1, DMA_CH2, DMA_INTF_FTFIF))
  ;

其中:

/*!< full transger finish flag */
#define DMA_INTF_FTFIF    BIT(5) 

函数说明

获取DMA标志

if(DMA_INTF1(dma_periph) & DMA_FLAG_ADD(flag,channelx)){
    return SET;
}else{
    return RESET;
}

其中:

/*!< DMA interrupt flag register 1 */
#define DMA_INTF1(dmax)    REG32((dmax) + 0x04U)        

/*!< DMA channel flag shift */
#define DMA_FLAG_ADD(flag,channel)  \
      ((uint32_t)((flag)<<((((uint32_t)(channel)*6U)) \
    + ((uint32_t)(((uint32_t)(channel)) >> 1U)&0x01U)*4U)))   

中断标志位寄存器 1 (DMA_INTF1)

地址偏移:0x04

名称 说明
27/21/11/5 FTFIFx 通道x的传输完成标志位(x=4…7)
硬件置位,软件写DMA_INTC0相应位为1清零
0 : 通道x传输未完成
1 : 通道x传输完成

2.4.7 比较结果

/* check the received data with the send ones */
transfer_status = memory_compare(tx_buffer , rx_buffer , 
                                  ARRAYNUM(tx_buffer));

2.5 I2C EEPROM

2.5.1 简介

I2C(内部集成电路总线)模块可用于 MCU 和外部 I2C 设备的通讯。I2C 总线使用两条串行线:串行数据线 SDA 和串行时钟线 SCL。

I2C 接口模块实现了 I2C 协议的标速模式和快速模式,具备 CRC 计算和校验功能、支持 SMBus (系统管理总线)和 PMBus(电源管理总线),此外还支持多主机 I2C 总线架构。I2C 接口模 块也支持 DMA 模式,可有效减轻 CPU 的负担。

SDA 和 SCL 都是双向线,通过一个电流源或者上拉电阻接到电源正极。当总线空闲时,两条线都是高电平。连接到总线的设备输出极必须带开漏或者开集,以提供线与功能。

I2C 通讯流程

  • I2C设备有唯一地址 每个I2C设备(不管是微控制器,LCD驱动,存储器或者键盘接口)都通过唯一的地址进行识别,根据设备功能,他们既可以是发送器也可作为接收器。
  • I2C从机响应主机命令
    • I2C总线上出现START起始位
    • 从机接收地址
    • 与自身地址对比
      • 地址相同
        • 返回ACK应答
        • 与主机互动:发送/接收数据
      • 地址不同:忽略

        I2C从机始终对一个广播地址 (0x00)发送确认应答

  • I2C主机传输数据
    • 产生START和STOP
    • 开始和结束一次传输
    • 产生SCL时钟

2.5.2 配置GPIO端口

启用GPIOB和I2C0时钟

/* enable GPIOB clock */
rcu_periph_clock_enable(RCU_GPIOB);
/* enable I2C0 clock */
rcu_periph_clock_enable(RCU_I2C0);

其中:

/*!< I2C0 clock */
RCU_I2C0 = RCU_REGIDX_BIT(APB1EN_REG_OFFSET, 21U),                 

连接GPIOB和SCL、SDA

/* connect PB6 to I2C0_SCL */
gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_6);
/* connect PB7 to I2C0_SDA */
gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_7);
  • GPIOB[6] -> I2C0_SCL
  • GPIOB[7] -> I2C0_SDA

设置GPIOB引脚的属性

gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GPIO_PIN_6);
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ,GPIO_PIN_6);
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GPIO_PIN_7);
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ,GPIO_PIN_7);
  • GPIOB[6/7]输出模式
    • 上拉电阻
    • 开漏模式
      • 输出控制寄存器设置为“0”时,相应引脚输出低电平;
      • 输出控制寄存器设置为“1”,相应管脚处于高阻状态;

2.5.3 配置I2C

配置I2C0时钟

/* enable I2C clock */
rcu_periph_clock_enable(RCU_I2C0);
/* configure I2C clock */
i2c_clock_config(I2C0,I2C0_SPEED,I2C_DTCY_2);

#define I2C0_SPEED   400000
/*!< I2C fast mode Tlow/Thigh = 2 */
#define I2C_DTCY_2   ((uint32_t)0x00000000U)                  

其中:

void i2c_clock_config(
        uint32_t i2c_periph, 
        uint32_t clkspeed, 
        uint32_t dutycyc)
  • 参数
    • i2c_periph: I2C外设控制器,I2Cx(x=0,1,2)
    • clkspeed:I2C时钟速率,支持
      • 标准模式(最高100KHz)
      • 快速模式(最高400KHz)
    • dutycyc:快速模式下的占空比
      • I2C_DTCY_2: T_low/T_high=2
      • I2C_DTCY_16_9: T_low/T_high=16/9
  1. 获取APB1时钟频率
     pclk1 = rcu_clock_freq_get(CK_APB1);
    
  2. 设置I2C外设的时钟频率
     freq = (uint32_t)(pclk1/1000000U);
     temp = I2C_CTL1(i2c_periph);
     temp &= ~I2C_CTL1_I2CCLK;
     temp |= freq;
        
     I2C_CTL1(i2c_periph) = temp;
    

    其中:

     /*!< I2C control register 1 */
     #define I2C_CTL1(i2cx)  REG32((i2cx) + 0x04U)      
    

    控制寄存器 1 (I2C_CTL1)

    地址偏移:0x04

    名称 说明
    5:0 I2CCLK I2C 外设时钟频率
    I2CCLK[5:0]应该是输入 APB1 时钟频率,最低2MHz
    000000 - 000001:无时钟
    000010 - 110010:2 MHz~50MHz
    注意
    在标准模式下,APB1 时钟频率需大于或者等于 2MHz。
    在快速模式下,APB1 时钟 频率需大于或者等于 8MHz
  3. clkspeed >= 40000
    • 在快速模式下,最大SCL上升时间为300ns
      I2C_RT(i2c_periph) = (uint32_t)((
            (freq*(uint32_t)300U) / (uint32_t)1000U)
            + (uint32_t)1U);
      
      /*!< I2C rise time register */
      #define I2C_RT(i2cx)   REG32((i2cx) + 0x20U)      
      

      上升时间寄存器 (I2C_RT) 地址偏移:0x20 复位值:0x0002

      名称 说明
      5:0 RISETIME[5:0] 主机模式下最大上升时间
      RISETIME 值应该为 SCL 最大上升时间加 1
    • 如果占空比是2
      • 计算clk时钟,设置I2C占空比为2
                
        clkc = (uint32_t)(pclk1/(clkspeed*3U));
        I2C_CKCFG(i2c_periph) &= ~I2C_CKCFG_DTCY;
        
        /*!< I2C clock configure register */
        #define I2C_CKCFG(i2cx)   REG32((i2cx) + 0x1CU)      
        

        时钟配置寄存器 (I2C_CKCFG)

      名称 说明
      15 FAST 主机模式下 I2C 速度选择
      0:标准速度
      1:快速
      14 DTCY 快速模式下占空比
      0:Tlow/Thigh = 2
      1:Tlow/Thigh = 16/9
      11:0 CLKC[11:0] 主机模式下 I2C 时钟控制
      标准速度模式下:Thigh = Tlow = CLKC ∗ TPCLK1
      如果 DTCY=0,快速模式下:
      Thigh = CLKC * TPCLK1 , Tlow = 2 * CLKC * TPCLK1
    • 设置主机模式下I2C时钟,并在I2C主机模式下选择快速模式
      I2C_CKCFG(i2c_periph) |= I2C_CKCFG_FAST;
      I2C_CKCFG(i2c_periph) |= clkc;
      
      /*!< I2C speed selection in master mode */
      #define I2C_CKCFG_FAST    BIT(15)       
      

      配置I2C地址

i2c_mode_addr_config(I2C0,  I2C_I2CMODE_ENABLE,
                    I2C_ADDFORMAT_7BITS,
                    I2C0_SLAVE_ADDRESS7);

/*!< I2C base address  */
#define I2C_BASE    (APB1_BUS_BASE + 0x00005400U)  
/*!< I2C0 base address */
#define I2C0        I2C_BASE                   

/*!< I2C mode */
#define I2C_I2CMODE_ENABLE   ((uint32_t)0x00000000U)  
/*!< address:7 bits */
#define I2C_ADDFORMAT_7BITS  ((uint32_t)0x00000000U) 
#define I2C0_SLAVE_ADDRESS7     0xA0

i2c_mode_addr_config说明:

void i2c_mode_addr_config(uint32_t i2c_periph, 
                          uint32_t mode, 
                          uint32_t addformat, 
                          uint32_t addr)
  • 参数
    • i2c_periph: I2Cx(x=0,1,2)
    • 模式
      • I2C_I2CMODE_ENABLE: I2C mode
      • I2C_SMBUSMODE_ENABLE: SMBus mode
    • addformat: 7bits or 10bits
      • I2C_ADDFORMAT_7BITS: 7bits
      • I2C_ADDFORMAT_10BITS: 10bits
    • addr: I2C 作为从机时的地址
  1. 设置模式
     ctl = I2C_CTL0(i2c_periph);
     ctl &= ~(I2C_CTL0_SMBEN); 
     ctl |= mode;
     I2C_CTL0(i2c_periph) = ctl;
    

    其中:

     /*!< I2C control register 0 */
     #define I2C_CTL0(i2cx)   REG32((i2cx) + 0x00U)      
     /*!< SMBus mode */
     #define I2C_CTL0_SMBEN   BIT(1)        
    

    控制寄存器 0 (I2C_CTL0)

    名称 说明
    1 SMBEN SMBus/I2C 模式开关
    0:I2C 模式
    1:SMBus 模式
  2. 设置地址
     addr = addr & I2C_ADDRESS_MASK;
    
     // 在7位地址模式下:I2C_SADDR0[7:1] = 0xA0
     // 所以从机地址为 0xA0>>1 = 80 = 0x50
     I2C_SADDR0(i2c_periph) = (addformat | addr);
    

    其中:

     /*!< I2C slave address register 0 */
     #define I2C_SADDR0(i2cx)    REG32((i2cx) + 0x08U)      
    

    从机地址寄存器 0 (I2C_SADDR0)

    地址偏移:0x08

    名称 说明
    15 ADDFORMA I2C 从机地址格式
    0:7 位地址
    1:10 位地址
    9:8 ADDRESS[9:8] 10 位地址的最高两位
    7:1 ADDRESS[7:1] 7 位地址或者 10 位地址的第 7-1 位
    0 ADDRESS0 10 位地址的第 0 位

启用I2C

i2c_enable(I2C0);

void i2c_enable(uint32_t i2c_periph) {
    I2C_CTL0(i2c_periph) |= I2C_CTL0_I2CEN;
}

/*!< peripheral enable */
#define I2C_CTL0_I2CEN  BIT(0) 
名称 地址
10 ACKEN ACK 使能
软件置 1 和清 0,当 I2CEN=0 时硬件清 0。
0:不发送 ACK
1:发送 ACK
0 I2CEN I2C 外设使能
0:禁用 I2C
1:使能 I2C

启用应答

i2c_ack_config(I2C0,I2C_ACK_ENABLE);

void i2c_ack_config(uint32_t i2c_periph, uint32_t ack)
{
    if(I2C_ACK_ENABLE == ack){
        I2C_CTL0(i2c_periph) |= I2C_CTL0_ACKEN;
    }else{
        I2C_CTL0(i2c_periph) &= ~(I2C_CTL0_ACKEN);
    }
}

/*!< acknowledge enable */
#define I2C_CTL0_ACKEN  BIT(10)  //BIT(10) = 1<<10
名称 地址
10 ACKEN ACK 使能
软件置 1 和清 0,当 I2CEN=0 时硬件清 0。
0:不发送 ACK
1:发送 ACK

2.5.4 初始化I2C EEPROM驱动使用的外围设备

void i2c_eeprom_init() {
    eeprom_address = EEPROM_BLOCK0_ADDRESS;
}

#define EEPROM_BLOCK0_ADDRESS    0xA0

2.5.5 I2C读写测试

i2crw

起始信号产生后,所有从机就开始等待主机紧接下来广播的从机地址信号(SLAVE_ADDRESS)。在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。 根据 I2C协议,这个从机地址可以是 7位或 10位。 在地址位之后,是传输方向的选择位,该位为 0 时,表示后面的数据传输方向是由主机传输至从机,即主机向从机写数据。该位为 1时,则相反。 从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号,只有接收到应答信号后,主机才能继续发送或接收数据。

写数据

若配置的方向传输位为“写数据”方向,即第一幅图,广播完地址,接收到应答信号后,主机开始正式向从机传输数据(DATA),数据包的大小为 8 位,主机每发送完一个字节数据,都要等待从机的应答信号(ACK),重复,可以向从机传输 N 个数据,这个 N 没有大小限制。当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。

读数据

若配置的方向传输位为“读数据”方向,即第二幅图,广播完地址,接收到应答信号后,从机开始向主机返回数据(DATA),数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号(ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。 读和写数据

复合

除了基本的读写,I2C 通讯更常用的是复合格式,即第三幅图,该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

初始化i2c_buff

for(i = 0;i < BUFFER_SIZE;i++){ 
    i2c_buffer_write[i]=i;
}

写EEPROM

写数据

若配置的方向传输位为“写数据”方向,广播完地址,接收到应答信号后,主机开始正式向从机传输数据(DATA),数据包的大小为 8 位,主机每发送完一个字节数据,都要等待从机的应答信号(ACK),重复,可以向从机传输 N 个数据,这个 N 没有大小限制。当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。

分析EEPROM的块写入函数
eeprom_buffer_write(i2c_buffer_write,
                    EEP_FIRST_PAGE, BUFFER_SIZE); 

#define EEP_FIRST_PAGE     0x00

函数说明: 向I2C EEPROM写入数据缓冲区

void eeprom_buffer_write(uint8_t* p_buffer, 
                         uint8_t write_address, 
                         uint16_t number_of_byte)
  • p_buffer: 指向包含要写入EEPROM的数据的缓冲区的指针
  • write_address: EEPROM的内部地址,用于写入
  • number_of_byte:写入EEPROM的字节数

按照I2C_PAGE_SIZE(8)的大小向EEPROM写入数据,因为wirte_address不一定页对齐,所以分成

  • 写入地址是页对齐的写入
  • 写入地址非页对齐
    • 写入长度在当前页内
    • 写入长度超出当前页
  1. 按照页的方式对齐要写入的内容
     // 要写入的地址与页的偏差
     address = write_address % I2C_PAGE_SIZE;
     // 空出的字节数
     count = I2C_PAGE_SIZE - address;
     number_of_page =  number_of_byte / I2C_PAGE_SIZE;
     number_of_single = number_of_byte % I2C_PAGE_SIZE;
    
  2. 如果写地址是PAGE对齐的
    • 首先按页批量写入
        while(number_of_page--){
            eeprom_page_write(p_buffer, write_address, I2C_PAGE_SIZE); 
            eeprom_wait_standby_state();
            write_address +=  I2C_PAGE_SIZE;
            p_buffer += I2C_PAGE_SIZE;
        }
      
    • 剩余的逐个写入
        if(0 != number_of_single){
            eeprom_page_write(p_buffer, write_address, number_of_single);
            eeprom_wait_standby_state();
        }      
      
  3. 如果写地址不是PAGE对齐
    • 如果写入内容的长度在一页之内,直接写
       IF(NUMBER_OF_BYTE < COUNT){ 
           EEPROM_PAGE_WRITE(P_BUFFER, WRITE_ADDRESS, NUMBER_OF_BYTE);
           EEPROM_WAIT_STANDBY_STATE();
       }
      
    • 否则,[addr, pagesize]部分按字节写;后面的按页写
       // 写入的bytes按本页边界分成两部分[addr, pagesize], [pagesize, ...]
       number_of_byte -= count; // number_of_byte是按页对齐的内容
       number_of_page =  number_of_byte / I2C_PAGE_SIZE;
       number_of_single = number_of_byte % I2C_PAGE_SIZE;
      
       // 这一部分没有页对齐的内容按字节写 [addr, pagesize]
       if(0 != count){
           eeprom_page_write(p_buffer, write_address, count);
           eeprom_wait_standby_state();
           write_address += count;
           p_buffer += count;
       } 
      
       // 后面部分是页对齐的,按页写
       /* write page */
       while(number_of_page--){
           eeprom_page_write(p_buffer, write_address, I2C_PAGE_SIZE);
           eeprom_wait_standby_state();
           write_address +=  I2C_PAGE_SIZE;
           p_buffer += I2C_PAGE_SIZE;
       }
       /* write single */
       if(0 != number_of_single){
           eeprom_page_write(p_buffer, write_address, number_of_single); 
           eeprom_wait_standby_state();
       }
      
分析EEPROM的页写入函数
void eeprom_page_write(uint8_t* p_buffer, uint8_t write_address, uint8_t number_of_byte)

在一个写入周期内向EEPROM写入一个以上的字节,把p_buffer内存中的number_of_byte字节写入EEPROM的write_address

  1. 等待I2C总线空闲

     while(i2c_flag_get(I2C0, I2C_FLAG_I2CBSY));
    
     /* I2Cx(x=0,1,2) definitions */
     /*!< I2C0 base address */
     #define I2C0    I2C_BASE                   
     /*!< busy flag */
     I2C_FLAG_I2CBSY = I2C_REGIDX_BIT(I2C_STAT1_REG_OFFSET, 1U)
     //相当于 I2C_STAT1_REG[1]
    
  2. 向I2C总线发送一个启动条件

     // 主机模式下发送 START 起始位
     I2C_CTL0(i2c_periph) |= I2C_CTL0_START;
    
  3. 等待SBSEND = 1
  4. 向I2C总线发送从机地址
  5. 等到ADDSEND = 1,
    • ADDSEND:主机模式下,成功发送了地址
  6. 清除ADDSEND位
  7. 等到发送数据缓冲区为空
  8. 发送要写入EEPROM内部的地址:只有一个字节的地址

     i2c_data_transmit(I2C0, write_address);
    
  9. 等到BTC = 1
    • BTC:字节发送结束
  10. 在有数据要写的情况下,循环写入数据

    while(number_of_byte--){  
        i2c_data_transmit(I2C0, *p_buffer);
            
        /* point to the next byte to be written */
        p_buffer++; 
            
        /* wait until BTC bit is set */
        while(!i2c_flag_get(I2C0, I2C_FLAG_BTC));
    }
    

    这里是一次写入一页(8个字节)的数据 其中:

    void i2c_data_transmit(uint32_t i2c_periph, uint8_t data) {
        I2C_DATA(i2c_periph) = DATA_TRANS(data);
    }
    
    /*!< I2C transfer buffer register */
    #define I2C_DATA(i2cx)          REG32((i2cx) + 0x10U)      
    #define DATA_TRANS(regval)      (BITS(0,7) & ((uint32_t)(regval) << 0))
    

    传输缓冲区寄存器 (I2C_DATA)

    名称 说明
    [7:0] TRB[7:0] 数据发送接收缓冲区
  11. 向I2C总线发送一个停止条件
  12. 等待停止条件完成

读EEPROM

读数据

若配置的方向传输位为“读数据”方向,即第二幅图,广播完地址,接收到应答信号后,从机开始向主机返回数据(DATA),数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号(ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。

eeprom_buffer_read(i2c_buffer_read,EEP_FIRST_PAGE, BUFFER_SIZE); 

#define EEP_FIRST_PAGE         0x00

从EEPROM的EEP_FIRST_PAGE处读取BUFFER_SIZE字节到i2c_buffer_read

分析EEPROM的块读取
  1. 等待I2C总线空闲

     while(i2c_flag_get(I2C0, I2C_FLAG_I2CBSY));
    

    如果只发送2个字节,设置下一个字节应答

     if(2 == number_of_byte){
         i2c_ackpos_config(I2C0,I2C_ACKPOS_NEXT);
     }
    
     I2C_CTL0(i2c_periph) |= I2C_CTL0_POAP;
    

    控制寄存器 0 (I2C_CTL0)

    名称 说明
    POAP ACK/PEC 的位置含义 0:ACKEN 位决定对当前正在接收的字节是否发送 ACK/NACK
    1:ACKEN 位决定是否对下一个字节发送 ACK/NACK
  2. 向I2C总线发送一个启动条件

     // 主机模式下发送 START 起始位
     I2C_CTL0(i2c_periph) |= I2C_CTL0_START;
    
  3. 等待SBSEND = 1
  4. 向I2C总线发送从属地址
  5. 等到ADDSEND = 1,
    • ADDSEND:主机模式下,成功发送了地址
  6. 清除ADDSEND位
  7. 等到发送数据缓冲区为空
  8. 使能I2C0

     i2c_enable(I2C0);
    
  9. 发送要读取EEPROM内部的地址:只有一个字节的地址

    i2c_data_transmit(I2C0, read_address);
    
  10. 等到BTC = 1
    1. BTC:字节发送结束
  11. 向I2C总线发送一个启动条件
```c
i2c_start_on_bus(I2C0);

// 主机模式下发送 START 起始位
// I2C_CTL0(i2c_periph) |= I2C_CTL0_START;
```
  1. 等待SBSEND = 1
  2. 向I2C总线发送从属地址

    i2c_master_addressing(I2C0, eeprom_address, I2C_RECEIVER);
    
    if(number_of_byte < 3){
        /* disable acknowledge */
        i2c_ack_config(I2C0,I2C_ACK_DISABLE);
    }
    
  3. 等到ADDSEND = 1,
    • ADDSEND:主机模式下,成功发送了地址
  4. 清除ADDSEND位

    i2c_flag_clear(I2C0,I2C_FLAG_ADDSEND);
        
    if(1 == number_of_byte){
        /* send a stop condition to I2C bus */
        i2c_stop_on_bus(I2C0);
    }
    
  5. 如果有数据要读取

    while(number_of_byte){
        if(3 == number_of_byte){
            /* wait until BTC bit is set */
            while(!i2c_flag_get(I2C0, I2C_FLAG_BTC));
    
            /* disable acknowledge */
            i2c_ack_config(I2C0,I2C_ACK_DISABLE);
        }
        if(2 == number_of_byte){
            /* wait until BTC bit is set */
            while(!i2c_flag_get(I2C0, I2C_FLAG_BTC));
                
            /* send a stop condition to I2C bus */
            i2c_stop_on_bus(I2C0);
        }
            
        /* wait until the RBNE bit is set and clear it */
        if(i2c_flag_get(I2C0, I2C_FLAG_RBNE)){
            /* read a byte from the EEPROM */
            *p_buffer = i2c_data_receive(I2C0);
                
            /* point to the next location where the byte read will be saved */
            p_buffer++; 
                
            /* decrement the read bytes counter */
            number_of_byte--;
        } 
    }
    

    其中

    I2C_FLAG_RBNE = I2C_REGIDX_BIT(I2C_STAT0_REG_OFFSET, 6U)
    
    uint8_t i2c_data_receive(uint32_t i2c_periph) {
        return (uint8_t)DATA_RECV(I2C_DATA(i2c_periph));
    }
    
    #define DATA_RECV(regval)    GET_BITS((uint32_t)(regval), 0, 7)
    

    传输状态寄存器 0 (I2C_STAT0) 地址偏移:0x14

    名称 说明
    6 RBNE 接收期间 I2C_DATA 非空
    硬件从移位寄存器移动一个字节到 I2C_DATA 寄存器之后将此位置 1,读 I2C_DATA 可以清除此位。
    如果 BTC 和 RBNE 都被置 1,读 I2C_DATA 将不会清除 RBNE,因为移位寄存器的字节将被立即移到 I2C_DATA
    0:I2C_DATA 为空
    1:I2C_DATA 非空,软件可以读
  6. 等到停止条件完成

    while(I2C_CTL0(I2C0)&0x0200);
    
  7. 启用应答

    i2c_ack_config(I2C0,I2C_ACK_ENABLE);
    i2c_ackpos_config(I2C0,I2C_ACKPOS_CURRENT);
    

比较读取和写入缓存区内容

for(i = 0;i < BUFFER_SIZE;i++){
    if(i2c_buffer_read[i] != i2c_buffer_write[i]){
        return I2C_FAIL;
    }
    return I2C_OK;
}

通过I2C控制PCA9535

原理图

  1. I2C2的CPU端口 i2c_cpu
  2. pca9535 pca9535
  3. pwr_swicth pwr_s

接线管脚

CPU管脚 I2C功能
PH7 I2C2-SCL
PH8 I2C2-SDA

配置I2C2

启用GPIOH和I2C2时钟

rcu_periph_clock_enable(RCU_GPIOH);
rcu_periph_clock_enable(RCU_I2C2);

连接GPIOH和SCL、SDA

/* PH7 -> I2C2-SCL */
gpio_af_set(GPIOH, GPIO_AF_4, GPIO_PIN_7);
/* PH8 -> I2C2_SDA */
gpio_af_set(GPIOH, GPIO_AF_4, GPIO_PIN_8);

设置GPIOH引脚的属性

gpio_mode_set(GPIOH, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GPIO_PIN_7);
gpio_output_options_set(GPIOH, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ,GPIO_PIN_7);
gpio_mode_set(GPIOH, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GPIO_PIN_8);
gpio_output_options_set(GPIOH, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ,GPIO_PIN_8);
  • GPIOH[7/8]输出模式
    • 上拉电阻
    • 开漏模式
      • 输出控制寄存器设置为“0”时,相应引脚输出低电平;
      • 输出控制寄存器设置为“1”,相应管脚处于高阻状态;

3 通用同步异步收发器(USART)

3.1 简介

3.1.1 USART 帧格式

  • USART_CTL0[WL]:设置数据长度
  • USART_CTL0[PCEN]:最后一个数据位用作校验位
  • USART_CTL0[PM]:选择校验位的计算方法
  • USART_CTL1[STB[1:0]]:停止位

3.1.2 USART 发送器

  • USART_CTL0[TE]:启用发送
    • 当发送数据缓冲区不为空时
    • 发送器将会通过TX引脚发送数据帧
    • USART_CTL3[TINV]设置Tx引脚极性
    • TEN置位后发送器会发出一个空闲帧
  • 发送过程
    • 系统上电后,TBE默认为高电平
    • USART_STAT0[TBE]=1时
      • 数据写入USART_DATA寄存器
      • USART_STAT0[TBE]=0
      • 数据由USART_DATA移入移位寄存器后
      • USART_STAT0[TBE]=1
    • 数据在发送过程中写入USART_DATA寄存器
      • 首先被存入发送缓冲区
      • 在当前发送过程完成后
        • 传输到发送移位寄存器
  • 生成中断
    • USART_CTL0[TCIE]=1:启用中断
    • USART_STAT0[TBE]=1的情况下
      • USART_STAT0[TC]=1(发送完成,实在确定TBE的情况下才能给出发送完成)
    • 此时产生中断

3.1.3 USART 接收器

  • 启用接收器
    • USART_CTL0[UEN]=1,启用UART
    • USART_CTL0[WL]确定接收字长
    • USART_CTL1[STB[1:0]]确定停止位长度
    • 如果选择了多级缓存通信方式
      • USART_CTL2[DENR]启用DMA
    • USART_BAUD设置波特率
    • USART_CTL0[REN]=1,启用接收器
  • 接收数据
    • 接收器在使能后若检测到一个有效的起始脉冲便开始接收码流
  • 中断
    • USART_CTL0[RBNEIE]=1,启用接收中断
    • 接收到一个数据帧
      • USART_STAT0[RBNE]=1
    • 产生中断
  • 读取数据
    • 读取USART_DATA
    • 通过DMA读取数据
    • 注意
      • 只要是对USART_DATA寄存器的一个读操作都可以清除RBNE位
      • 在接收过程中,需使能REN位,不然当前的数据帧将会丢失
    • 异常
      • 当接收到一帧数据,而RBNE位还没有被清零
        • 随后的数据帧将不会存储在数据接收缓冲区中
        • USART_STAT0[ORERR]=1,溢出错误标志位

3.1.4 DMA 方式访问数据缓冲区

  • DMA访问发送缓冲区或者接收缓冲区可以减轻CPU负担
  • DMA发送
    • USART_CTL2[DENT]=1
    • DMA 将数据从片内 SRAM 传送到 USART 的数据缓冲区
    • 所有数据帧都传输完成后
      • USART_STAT0[TC]=1
  • 启用DMA接收
    • USART_CTL2[DENR]=1
    • DMA 将数据从接收缓冲区传送到片内 SRAM
    • 若USART_CTL2[ERRIE]=1
      • USART_STAT0 寄存器中的错误标志位会产生中断

附录

时钟

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

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

系统时钟(CK_SYS)选择

  • 系统复位后,IRC16M时钟默认做为CK_SYS的时钟源,改变配置寄存器0,RCU_CFG0中的 系统时钟变换位SCS可以切换系统时钟源为HXTAL或CK_PLLP

GD32常用函数

  1. rcu_periph_clock_disable
    • 关闭外设时钟
  2. 给外设寄存器的控制位置1
      void rcu_periph_clock_enable(rcu_periph_enum periph) {
       RCU_REG_VAL(periph) |= BIT(RCU_BIT_POS(periph));
      }
    
    • periph = regaddr << 6 | ctl_bit
    • 高位记录了这个外设所在的寄存器地址
    • 低5位记录了这个外设的控制位所在的位置 - RCU_REG_VAL(periph):与外设periph有关的控制字寄存器变量(32位 *reg) - BIT(RCU_BIT_POS(periph))
    • RCU_BIT_POSperiph中取出控制外设periph的位的位置
      • 比如:打开USART0的控制位是第4位,则RCU_BIT_POS(RCU_USART0) = 4
    • BIT(4) = 1 << 4

说明

void rcu_periph_clock_disable(rcu_periph_enum periph)
{
    RCU_REG_VAL(periph) &= ~BIT(RCU_BIT_POS(periph));
}
typedef enum {
    /* AHB1 peripherals */
    RCU_GPIOA     = RCU_REGIDX_BIT(AHB1EN_REG_OFFSET, 0U),    /*!< GPIOA clock */
    RCU_USART0    = RCU_REGIDX_BIT(APB2EN_REG_OFFSET, 4U),    /*!< USART0 clock enable */
} rcu_periph_enum;

// APB2 使能寄存器 (RCU_APB2EN),偏移APB2EN_REG_OFFSET(=0x44),
// RCU_APB2EN[4]:USART0_EN
// RCU_USART0 = 0x44 << 6 | 4
// RCU_USART0 = USART0控制寄存器地址偏移 << 6 | USART0 使能位编号
// 1 << 5 = 32,所以低5位就可以记录[0, 31]


// AHB1 使能寄存器 (RCU_AHB1EN)
// 地址偏移:0x30
#define AHB1EN_REG_OFFSET    0x30U    /*!< AHB1 enable register offset */
#define APB2EN_REG_OFFSET    0x44U    /*!< APB2 enable register offset */
#define RCU_REGIDX_BIT(regidx, bitpos)    (((uint32_t)(regidx) << 6) | (uint32_t)(bitpos))

// 第x位的值
#define BIT(x)               ((uint32_t)((uint32_t)0x01U<<(x)))
// 取val的后5位
#define RCU_BIT_POS(val)     ((uint32_t)(val) & 0x1FU)

#define RCU                  RCU_BASE
#define AHB1_BUS_BASE        ((uint32_t)0x40020000U)        /*!< ahb1 base address */
#define RCU_BASE             (AHB1_BUS_BASE + 0x00003800U)  /*!< RCU base address */
// RCU外设寄存器的值 
#define RCU_REG_VAL(periph)  (REG32(RCU + ((uint32_t)(periph) >> 6)))
#define REG32(addr)          (*(volatile uint32_t *)(uint32_t)(addr))

systick_config函数分析

在ARM Cortex-M4内核中有一个Systick定时器,一个24bit的倒计数定时器,当计数到0时,它就会从Load寄存器中自动重装定时初值,只要不把CTRL寄存器中的ENABLE清0,它就永不停

__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
    /* ticks参数有效性检查 */
  if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk)  return (1);

  /* 设置重装载值, -1:因为装载时消耗掉一个systick时钟周期*/
  SysTick->LOAD  = ticks - 1;                                  
  /* set Priority for Systick Interrupt */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  

  /* 初始化VAL=0,使能Systick后立刻进入重装载 */
  SysTick->VAL   = 0;                                          

  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | /* 选择时钟源 */
                   SysTick_CTRL_TICKINT_Msk   | /* 开启Systick中断 */
                   SysTick_CTRL_ENABLE_Msk;     /* 使能Systick定时器 */
  return (0);   /* Function successful */
}

SysTick->LOAD = ticks - 1

要产生一个周期为N个处理器时钟周期的多拍定时器,使用RELOAD值为N-1。如果每100个时钟脉冲需要SysTick中断,则将RELOAD设为99

SysTick_IRQn   = -1,     /*!< 15 Cortex-M4 system tick interrupt */
#define __NVIC_PRIO_BITS   4    /*!< GD32F4xx uses 4 bits for the priority levels */

NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1)
NVIC_SetPriority (-1, 15)

NVIC_SetPriority实际就是把priority写到管理xxx_IRQn优先级寄存器的地址内,比如NVIC_SetPriority(SysTick_IRQn, 15)就是设置SysTick_IRQn的优先级是15,NVIC_SetPriority(SysTick_IRQn, 0)就是设置SysTick_IRQn的优先级是0,越小越高

__STATIC_INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
  if(IRQn < 0) {
  /* 设置内核的中断优先级 */
    SCB->SHP[((uint32_t)(IRQn) & 0xF)-4] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff); } 
  else {
  /* 设置设备的中断优先级 */
    NVIC->IP[(uint32_t)(IRQn)] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff);    }        
}
SCB->SHP[((uint32_t)(IRQn) & 0xF)-4] = ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff);
SCB->SHP[11] = (0b1111 << 4 & 0xff)

这里SCB->SHP内容如下:

systick

无论是M0+或者是M4内核,他们的中断优先级都是由NVICSCB(System Control Block)两个寄存器来管理的,ARM的中断源分为内核中断和IRQ中断,而对IRQ的中断管理是由NVIC来主导的,内核的中断管理则是由SCB来主导的。

NVIC_IPR0-NVIC_IPR59寄存器为每个中断提供一个8位的优先级字段,每个寄存器拥有四个优先级字段。这些寄存器是可以通过字节访问的。如图所示。

nvic

每个实现定义的优先级字段可以容纳一个优先级值,0-255。值越低,相应的中断的优先级越高。寄存器的优先级值字段为8位宽,非实现的低阶位读为0,忽略写入。

在使用CMSIS时要访问NVIC寄存器,请使用以下函数:

CMSIS 函数 说明
void NVIC_EnableIRQ(IRQn_Type IRQn) 启用一个中断或异常
void NVIC_DisableIRQ(IRQn_Type IRQn) 关闭一个中断或异常
void NVIC_SetPendingIRQ(IRQn_Type IRQn) 将中断或异常的等待状态设置为1
void NVIC_ClearPendingIRQ(IRQn_Type IRQn) 将中断或异常的等待状态清除为0
uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn) 读取中断或异常的挂起状态。如果待处理状态被设置为1,则该函数返回非零值
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority) 将一个具有可配置优先级的中断或异常的优先级设置为1
uint32_t NVIC_GetPriority(IRQn_Type IRQn) 读取具有可配置优先级的中断或异常的优先级。该函数返回当前的优先级

M4最高支持16+4*60=256个中断源,第二这60个寄存器是可以按字节操作的,第三个跟M0+一样即值越小优先级越高,最后则是最大的区别,即每个寄存器的8位数据都是可以定义其中断优先级了,PRIGROUP定义了这8位数据到底该怎么用,这里出现了两个新名词,即Group Priority和Subpriority,,从下面两张图可以看到在M4内核中,其对中断优先级管理是分了两个部分,一个是组优先级一个是子优先级,即组优先级是管理抢占优先级的(即是否能嵌套),即高的组优先级中断(数值低)可以抢占低的组优先级(数值高)中断的,而如果组优先级是一样的,即使子优先级比正在执行的中断的子优先级高也是不能抢占的,那又有同志们要问问题了,那子优先级有啥用?呵呵,正如图2所说,在组优先级一致的情况下,多个中断请求同时发生,这样的情况下子优先级高的可以先执行的,而子优先级低的则只能暂时pending等着了。而回到PRIGROUP的作用是用来配置NVIC的8位数据域是如何分配给抢占优先级和子优先级的,而一般情况下,最好是各留4位给这二位大爷和小爷了,即每位爷最多可配16个优先级,而PRIGROUP是从哪来的呢,呵呵,实际上它是SCB_AIRCR寄存器的其中3位如图3所示。

通过 void NVIC_SetPriorityGrouping(u32 PriorityGroup)可以设定SCB->AIRCR[10:8]的值,这个值是PRIGROUP的设定值,PRIGROUP字段表示二进制点的位置,它将中断优先级寄存器中的PRI_n字段分割成独立的组优先级和子优先级字段。

prigroup

这里SysTick->CTRL的定义,可以在CMSIS的文档《DUI0553A_cortex_m4_dgug》里查到:

名称 功能
[31:17] - 保留
[16] COUNTFLAG 如果自上次读取后定时器计数到0,则返回1
[15:3] - 保留
[2] CLK Source 时钟源:0 - 外部;1 - 内部
[1] TICK INT 启用SysTick异常请求
0 = 倒数到零不会发出SysTick异常请求
1 = 倒数到零时发出SysTick异常请求
软件可以使用COUNTFLAG来确定SysTick是否曾经计数到零。
[0] ENABLE 使能计数器:0 = 关闭; 1 = 使能

当ENABLE被设置为1时,计数器从SYST_RVR寄存器加载RELOAD值,然后向下计数。当达到0时,它将COUNTFLAG设置为1,并根据TICKINT的值,选择性地发出SysTick信号。然后它再次加载RELOAD值,并开始计数。

这里

SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | /* 选择时钟源 */
                  SysTick_CTRL_TICKINT_Msk   | /* 开启Systick中断 */
                  SysTick_CTRL_ENABLE_Msk;     /* 使能Systick定时器 */
  • SysTick_CTRL_CLKSOURCE_Msk = 1 << 2:使用的内部时钟源
  • SysTick_CTRL_TICKINT_Msk = 1<< 1:倒数到零时发出SysTick异常请求
  • SysTick_CTRL_ENABLE_Msk = 1<< 0:使能Systick定时器

寄存器的定义,在CMSIS/core_m4.h中:

typedef struct
{
  __IO uint32_t CTRL;   /*!< Offset: 0x000 (R/W)  SysTick Control and Status Register */
  __IO uint32_t LOAD;   /*!< Offset: 0x004 (R/W)  SysTick Reload Value Register       */
  __IO uint32_t VAL;    /*!< Offset: 0x008 (R/W)  SysTick Current Value Register      */
  __I  uint32_t CALIB;  /*!< Offset: 0x00C (R/ )  SysTick Calibration Register        */
} SysTick_Type;

结构体被定义为SysTick

#define SCS_BASE       (0xE000E000UL)         /*!< System Control Space Base Address  */
#define SysTick_BASE   (SCS_BASE +  0x0010UL)      /*!< SysTick Base Address */
#define SysTick   ((SysTick_Type * ) SysTick_BASE)  /*!< SysTick configuration struct */

设定步骤

  1. systick_clksource_set选择时钟源,
      void systick_clksource_set(uint32_t systick_clksource)
      {
       if(SYSTICK_CLKSOURCE_HCLK == systick_clksource ){
           /* set the systick clock source from HCLK */
           SysTick->CTRL |= SYSTICK_CLKSOURCE_HCLK;
       }else{
           /* set the systick clock source from HCLK/8 */
           SysTick->CTRL &= SYSTICK_CLKSOURCE_HCLK_DIV8;
       }
      }
    
    • 所在文件: GD32F4xx_Firmware_Library/GD32F4xx_standard_peripheral/Source/gd32f4xx_misc.c
    • 其中这两种时钟源:
    • SysTick_CLKSource_HCLK_Div8 外部时钟 72/8=9M    - SysTick_CLKSource_HCLK 内部时钟 HCLK=72M    - 这些时钟在GD32F4xx_Firmware_Library/CMSIS/GD/GD32F4xx/Source/system_gd32f4xx.c文件中,定义为:    - #define __SYSTEM_CLOCK_200M_PLL_25M_HXTAL (uint32_t)(200000000)
  2. 设定重载数
  3. 开启中断
  4. 启动滴答定时器

附录

CT75 温度传感器

CT75是一个数字温度传感器,精度为±0.5°C。温度数据可以通过2个数字接口(兼容SMBus、I C或2线)由MCU、蓝牙芯片或SoC芯片直接读出。 CT75支持IC通信,速度高达400 kHz。每颗芯片在出厂前都经过特别校准,在0°C至50°C范围内的精度为±0.5°C(最大)。 准确度在0°C至50°C的范围内,在工厂中进行了特别校准,然后再运给客户。对于±0.5°C的精度,不再需要重新校准。 它包括一个高精度的带隙电路,一个能提供0.0625C分辨率的12位模拟到数字转换器。

管脚说明

PIN 编号 PIN名称 说明
1 SDA 数字接口数据输入或输出引脚,需要一个上拉电阻至VCC
2 SCL 数字接口时钟输入引脚,需要一个上拉电阻到VCC
3 ALERT 通过设置T_HIGH /T_LOW寄存器来显示温度过高或过低的警报,它是开漏输出,具有可编程的低电平或高电平。在应用中需要一个上拉电阻到VCC。
4 GND -
5 A2  
6 A1 地址选择管脚,芯片可以被定义成32中不同的从机地址
7 A0  
8 VCC 电源管脚

功能说明

该芯片可以感知温度,并通过一个12位的ADC将其转换为数字数据。同时,该芯片支持可编程的高/低限温度设置。如果测量的温度达到或超过高限温度,ALERT引脚将有效(被设置为低电平或高电平,取决于配置寄存器的POL位)。一旦测量的温度低于低限温度(可由用户编程),ALERT引脚将被释放(比较器模式)。

温度数据的数字输出

温度测量数据被存储在只读温度寄存器中。温度寄存器是12位二进制格式(将EM位设置为’0’)或13位二进制格式(将EM位设置为’1’)的2字节。这2个字节的温度数据必须在每个读取周期中同时读取,第一字节是MSB,其次是第二字节,即LSB。

temperature

温度高于128

当温度高于128℃时,可以通过设置EM位为’1’来表示二进制寄存器数据,如上表2所示。在扩展格式中,AD转换器的分辨率没有变化,但增加了’+128’位。例如,100°C的12位格式是0x6400,其中0x64来自第一字节,0x00来自第二字节。100°C的13位格式是0x3201,其中0x32来自第1字节,0x01来自第2字节。上电复位后,EM位的默认值为’0’。

寄存器

rmap

芯片有4个寄存器,地址从0x00到0x03,有两个字节(第一字节和第二字节),每个寄存器共有16位,如下表所示。

寄存器描述

  1. 温度 tdr
  2. 配置 config
    • OS:One shot Conversion bit(一键转换位)
    • 当设备处于关机模式时,将该位设置为’1’将触发一次温度的转换。
    • 在转换过程中,OS位读为’0’。一旦完成了单次转换,器件就会返回到关机模式。当不需要连续监测温度时,该功能用于降低功耗。 - CR1, CR0, Conversion Rate Selection bits
    • 这2位允许用户为温度设置不同的转换率。默认情况下,POR后为00,也就是说,转换率为8Hz,即每秒转换8次。 - AL
    • AL位表示具有只读属性的警报状态
    • 该位总是作为POL位的反转来读
      • 当POL位等于0时,AL位读为1,直到测量的温度或等于或超过温度(高)的连续故障的编程数量,导致AL位读为0。
      • 反之,当POL位为’1’时,AL位读为0,直到温度等于或超过温度(高)的连续故障的编程数量。而一旦温度低于温度(低),AL位又被设置为0
    • TM位的状态不影响AL位的状态
  3. Low_Temp_Set寄存器 ltr
    • 设置低温门限寄存器
    • 寄存器地址:0x2
    • 默认值:0x4B (1st Byte), 0x00(2nd Byte)
    • 高/低限温度数据由High_Temp_Set寄存器[0x03]和Low_Temp_Set寄存器[0x02]决定
    • 其格式与Temp_Data寄存器[0x00]相同,可以是12位或13位二进制格式。
    • 芯片在每个转换周期中比较Temp_Data[0x00]寄存器和High_Temp_Set寄存器[0x03]/Low_Temp_Set寄存器[0x02],这将影响ALT引脚输出。
    • 默认值为0x4B00,12位二进制格式,意味着75C
  4. High_Temp_Set 高温门限寄存器 htr
    • 寄存器地址:0x3
    • 默认值:0x50(1st) 0x00(2nd)a
  5. 二进制补码
    • 1 1101求源码:保留符号位1, 数值为取反+1:得到:1 0011 = -3

INA220采用双线接口的电流/功率监控器

INA220是一个具有两线制接口的分流和电源监视器。INA220同时监测分流器压降和电源电压。一个可编程的校准值与一个内部乘法器相结合,可以直接读出安培数。一个额外的乘法寄存器可计算出瓦特的功率。双线接口具有16个可编程地址。INA220的独立分流输入使其可用于低端感应系统。 INA220通过总线上的分流器进行感应,其电压范围为0V至26V,对低端感应或CPU电源非常有用。该器件使用单一的+3V至+5.5V电源,最大消耗1mA的电源电流。INA220的工作温度为-40℃至+125℃。

ina220

从INA220中读写

访问INA220上的特定寄存器是通过向寄存器指针写入适当的值来完成的。寄存器的完整列表和相应的地址请参考表2。如图17所示,寄存器指针的值是在从机地址字节之后传输的第一个字节,R/W位为低电平。对INA220的每一次写操作都需要给寄存器指针一个值。

  • 主站发送从站地址(写)
  • 从站返回ACK
  • 主站发送寄存器地址(写)
    • 从站收到,并更新寄存器指针
  • 主站发送两个字节
    • 从站收到,并写入寄存器
  • 从站每接到一个字节,返回ACK
  • 主站通过产生START发起数据传输
  • 主站产生STOP结束数据传输

写寄存器从主站传送的第一个字节开始。这个字节是从属地址,R/W位为低电平。然后INA220确认收到一个有效的地址。主站传输的下一个字节是要写入数据的寄存器的地址。这个寄存器地址值会更新寄存器指针指向所需的寄存器。接下来的两个字节被写入由寄存器指针寻址的寄存器。INA220会确认收到每个数据字节。主站可以通过产生一个START或STOP条件来终止数据传输。

  • 主站发送从站地址(写)
  • 主站发送寄存器地址(写)
  • 主站产生START条件
  • 主站发送从站地址(读,bit[0]=1)
  • 从站传送寄存器的MSB
    • 主站返回ACK
    • 从站传送寄存器的LSB
    • 主站ACK

当从INA220读取时,通过写操作存储在寄存器指针中的最后一个值决定了在读操作中读取哪个寄存器。要改变读操作的寄存器指针,必须向寄存器指针写入一个新值。这种写入是通过发出一个R/W位为低的从属地址字节(写),然后是寄存器指针字节来完成的。不需要额外的数据。然后主站产生一个START条件,并发送R/W位为高的从站地址字节,以启动读命令。下一个字节由从属机构传送,是由寄存器指针指示的寄存器中最重要的字节。这个字节之后是主站的确认;然后从站传送最小有效字节。主站确认收到该数据字节。主站可以在收到任何数据字节后产生一个 “非确认”,或者产生一个 “开始 “或 “停止 “条件,从而终止数据传输。如果需要从同一个寄存器中重复读取数据,则不需要持续发送寄存器指针字节;INA220会保留寄存器指针值,直到它被下一次写入操作所改变。

基本的ADC功能

INA220的两个模拟输入,VIN+和VIN-,连接到感兴趣的总线上的一个分流电阻。总线电压在V_BUS引脚处测量。INA220通常由+3V至+5.5V的独立电源供电。被感应的总线可以从0V到26V变化。对于电源排序没有特别的考虑(例如,在电源电压关闭的情况下可以出现总线电压,反之亦然)。INA220感测分流器上的小压降以获得分流电压,并感测V_BUS相对于地的电压以获得总线电压。

当INA220处于正常工作模式时(即配置寄存器的MODE位被设置为’111’),它连续转换分流电压,直至分流电压平均功能(配置寄存器,SADC位)中设置的数字。然后,设备将母线电压转换为母线电压平均值中设定的数字(配置寄存器,BADC位)。配置寄存器中的模式控制也允许选择只转换电压或电流的模式,可以是连续的,也可以是响应一个事件(触发)的。

所有的电流和功率计算都在后台进行,不影响转换时间;电气特性表中显示的转换时间可用于确定实际的转换时间。

电流和总线电压在不同的时间点进行转换,这取决于分辨率和平均模式的设置。例如,当配置为12位和128个采样平均时,这两个值的采样时间最长可达68ms。同样,这些计算是在后台进行的,不会增加整个转换时间 。

INA220寄存器框图

INA220寄存器

其中:

  • PGA:量程范围

参考

  1. 《STM32系统学习——I2C(读写EEPROM)》