超低成本CH32V003评估学习板入门学习教程

超低成本CH32V003评估学习板入门学习教程

第一部分、序

由于作者水平有限,文档和视频中难免有出错和讲得不好的地方,欢迎各位读者和观众善意地提出意见和建议,谢谢!

第二部分、硬件概述

以下资料摘抄自官网:

https://www.wch.cn/products/CH32V003.html?

2.1概述

CH32V003系列是基于青稞RISC-V2A内核设计的工业级通用微控制器,支持48MHz系统主频,具有宽压、单线调试、低功耗、超小封装等特点。CH32V003系列内置1组DMA控制器、1组10位模数转换ADC、1组运放比较器、多组定时器以及标准通讯接口USART、IIC、SPI等。

2.2系统框图

img2.3产品特点

  • 青稞32位RISC-V2A处理器,支持2级中断嵌套
  • 最高48MHz系统主频
  • 2KB SRAM,16KB Flash
  • 供电电压:3.3/5V
  • 多种低功耗模式:睡眠、待机
  • 上/下电复位、可编程电压检测器
  • 1组1路通用DMA控制器
  • 1组运放比较器
  • 1组10位ADC
  • 1个16位高级定时器和1个16位通用定时器
  • 2个看门狗定时器和1个32位系统时基定时器
  • 1个USART接口、1组IIC接口、1组SPI接口
  • 18个I/O口,映像一个外部中断
  • 64位芯片唯一ID
  • 串行单线调试接口
  • 封装形式:TSSOP20、QFN20、SOP16、SOP8

2.4选型指南

image-20221015095750090

第三部分、基础篇——入坑实例

3.1 Eg1_LED_GPIO_Output

我们所有的实例基本都是基于官方评估板学习的,并且所有实例均参考官方例程修改而来,现在先来利用GPIO要控制LED,主要实现两颗LED的跑马灯效果;

3.1.1硬件设计:

硬件过于简单,这里展示一下原理图,根据原理图和实物图可知,两颗灯是通过跳线的方式灵活的让你分配引脚的;这里选择PC4和PC3,不要问为什么,因为我们替代的项目用到的就是PC4和PC3来控制两颗LED,懒得改硬件,由原理图可知,LED是共阳极接法,共阳极接法就是逻辑1灭灯,逻辑0亮灯,故而只要控制GPIO输出高低即可实现LED的亮灭;

image

3.1.2 软件设计:

软件涉及到新建工程的问题,大家自行参考MounRiver Studio,过程过于简单,不可描述!所以不展示!

跑马灯实验我们主要用到的固件库文件是:

ch32v00x_gpio.c/ch32v00x_gpio.h

需要驱动LED,就要配置GPIO口,下面是LED GPIO配置为推挽输出的代码;

该函数的功能就是用来实现配置 PC3和 PC4 为推挽输出。这里需要注意的是:在配置 CH32V003外设的时候,任何时候都要先使能该

外设的时钟。GPIO 是挂载在 APB2 总线上的外设,在固件库中对挂载在 APB2 总线上的外设时钟使能是通过函数RCC_APB2PeriphClockCmd()来实现的。

最后两行代码"LED1_OFF();LED2_OFF();"乃是为了初始化LED为关闭状态;

void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    LED1_OFF();
    LED2_OFF();
}

我们不妨打开GPIO_Init,可以看到初始化GPIO口其实就是配置CFGLR,我们再来看这个寄存器的定义;

![image-20221015145805577](image

上图是CFGLR寄存器的的描述,MODEy配置的是输入或输出;CNFy描述的是什么模式下的输入和什么模式下的输出;我们用以驱动LED的,当然选择输出模式并且选择推挽输出模式;y=3或4,故而MODE3=11B和CNF3=00B,MODE4=11B和CNF5=00B;

根据对LED的开关,其实就是对GPIO口的拉高或拉低;于是就有以下代码

#define     LED1_OFF()       GPIO_SetBits(GPIOC,GPIO_Pin_3)
#define     LED1_ON()       GPIO_ResetBits(GPIOC,GPIO_Pin_3)
#define     LED2_OFF()       GPIO_SetBits(GPIOC,GPIO_Pin_4)
#define     LED2_ON()       GPIO_ResetBits(GPIOC,GPIO_Pin_4)

void LED_Handle(void)
{
    LED1_ON();
    LED2_OFF();
    Delay_Ms(500);
    LED1_OFF();
    LED2_ON();
    Delay_Ms(500);

}

宏定义中的两个函数GPIO_SetBits和GPIO_ResetBits也是同样操作的是寄存器, 在参考手册中描述如下
image

image

BSHR是置位寄存器,写1有效,故而可以拉高对应的引脚;BCR是清零寄存器,写1可以拉低对应的引脚;

3.1.3 下载验证:

我们就可以把代码下载到开发板上,看看运行结果是否与我们要求的一致。运行结果是LED1和LED2以大约1Hz频率交替点亮熄灭;具体现象可以查看视频;

至此,我们的3.1小结第一个实例的学习就结束了,本章作为 CH32003 的入门第一个例子,详细介绍了

CH32003 的 IO 口操作,希望大家好好理解一下。

3.1.4 学习视频:

本节的视频链接如下:

CH32V003硬件概述和配置GPIO配置推挽输出实现跑马灯

3.2 Eg2_Key_GPIOInput

上一章,我们介绍了CH32V003 的 IO 口作为输出的使用,这一章,我们将向大家介绍如何使用 CH32V003 的 IO 口作为输入用。在本章中,我们将利用PC1与PC2,来控制板载的两个 LED 的亮灭。通过本章的学习,你将了解到 CH32V003 的 IO 口作为输入口的使用方法。

3.2.1硬件设计:

硬件过于简单,这里展示一下原理图,根据原理图和实物图可知,两颗灯是通过跳线的方式灵活的让你分配引脚的;这里选择PC4和PC3,按键的输入我们选用PC1和PC2作为KEY1 和KEY2 的IO;这里需要注意的是:KEY1 和 KEY2 是低电平有效的,并且外部都没有上下拉电阻,所以,需要在 CH32V003 内部设置上拉。image

3.2.2 软件设计:

打开我们的按键实验工程可以看到,我们引入了 key.c 文件以及头文件 key.h。下面我们首先打开 key.c 文件,代码如下:

#ifndef MYBSP_KEY_C_
#define MYBSP_KEY_C_


void Key_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
}


void Key_Handle(void)
{
    static uint8_t bounce=0,bounce1=0,i = 0;
    if(KEY1)
    {
        bounce=1;
    }else{
        if(bounce)
        {
            bounce=0;
            GPIO_WriteBit(GPIOC, GPIO_Pin_3, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET));
        }
    }

    if(KEY2)
    {
        bounce1=1;
    }else{
        if(bounce1)
        {
            bounce1=0;
            GPIO_WriteBit(GPIOC, GPIO_Pin_4, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET));
        }
    }
    Delay_Ms(10);
}



#endif /* MYBSP_KEY_C_ */

这段代码包含 2 个函数,void Key_Init(void)和 void Key_Handle(void),KEY_Init()是用来初始化按键输入的 IO 口的。首先使能 GPIOC 时钟,然后实现 PC1、PC2 的输入设置,这里和上节的输出配置差不多,只是这里用来设置成的是输入而上节是输出。Key_Handle()函数,则是用来扫描这2个 IO 口是否有按键按下。Key_Handle()函数,支持释放响应方式,这样的好处就是可以防止按一次多次触发,而坏处就是在需要长按的时候比较不合适。

Key_Handle在扫描到按键有动作时候就是对相应的LED进行翻转;

我们不妨打开GPIO_Init,可以看到初始化GPIO口其实就是配置CFGLR,我们再来看这个寄存器的定义;

image

上图是CFGLR寄存器的的描述,MODEy配置的是输入或输出;CNFy描述的是什么模式下的输入和什么模式下的输出;我们用以扫描按键的,当然选择输入模式并且选择上拉模式;y=1或2,故而MODE1=00B和CNF3=01B,MODE4=00B和CNF5=01B;

接下来我们看看头文件 key.h 里面的代码:

/*
 * Key.h
 *
 *  Created on: Oct 19, 2022
 *      Author: Administrator
 */

#ifndef MYBSP_KEY_H_
#define MYBSP_KEY_H_

#include "debug.h"

#define     KEY1       GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_1)
#define     KEY2       GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_2)
void Key_Init(void);
void Key_Handle(void);

#endif /* MYBSP_KEY_H_ */

这段代码里面最关键就是 2 个宏定义:

#define     KEY1       GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_1)
#define     KEY2       GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_2)

这里我们采取的是库函数的读取IO 口的值。库函数的本质就是操作寄存器,跳转到定义可以看到它是读取INDR寄存器的:

/*********************************************************************
 * @fn      GPIO_ReadInputDataBit
 *
 * @brief   GPIOx - where x can be (A..G) to select the GPIO peripheral.
 *
 * @param    GPIO_Pin - specifies the port bit to read.
 *             This parameter can be GPIO_Pin_x where x can be (0..15).
 *
 * @return  The input port pin value.
 */
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
    uint8_t bitstatus = 0x00;

    if((GPIOx->INDR & GPIO_Pin) != (uint32_t)Bit_RESET)
    {
        bitstatus = (uint8_t)Bit_SET;
    }
    else
    {
        bitstatus = (uint8_t)Bit_RESET;
    }

    return bitstatus;
}

用库函数实现的好处是在各个 CH32 芯片上面的移植性非常好,不需要修改任何代码。

最后,我们看看 main.c 里面编写的主函数代码如下:

/*********************************************************************
 * @fn      main
 *
 * @brief   Main program.
 *
 * @return  none
 */
int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    Delay_Init();
    USART_Printf_Init(115200);
    printf("Demo:Eg2_Key_GPIOInput\r\n");
    printf("SystemClk:%d\r\n",SystemCoreClock);
    LED_Init();
    Key_Init();
    while(1)
    {
        Key_Handle();

    }
}

主函数代码比较简单,先进行一系列的初始化操作,然后在死循环中调用按键扫描函数Key_Handle()扫描按键值,最后根据按键值控制 LED 的翻转。

3.2.3 下载验证:

按下KEY1,LED1翻转,按下KEY2,则是LED2翻转;

至此,我们的本节的学习就结束了。本节,作为CH32 的入门第三个例子,介绍了 CH32的 IO 作为输入的使用方法,同时巩固了前面的学习。希望大家在开发板上实际验证一下,从而加深印象。

3.3 Eg3_USART

前面几节介绍了 CH32V003的 IO 口操作。这一节我们将学习 CH32V003的串口,教大家如何使用 CH32V003的串口来发送和接收数据。本章将实现如下功能:CH32V003通过串口和上位机的对话,CH32V003在收到上位机发过来的字符串后,原原本本的返回给上位机。

串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,其重要性不言而喻。现在基本上所有的 MCU 都会带有串口,CH32V003自然也不例外。

3.3.1硬件设计:

硬件过于简单,这里展示一下原理图,USART1 TX-->D.5 RX-->D.6。image

3.3.2 软件设计:

本节的代码设计,我们直接在MRS(MounRiver Studio)建立的工程目录Debug下面的debug.c,debug.h上修改。这里我们对代码部分稍作讲解。打开串口实验工程,我们就可以看到上述两个文件里面的代码,
先介绍 USART_Printf_Init函数,该函数代码如下:

/*********************************************************************
 * @fn      USART_Printf_Init
 *
 * @brief   Initializes the USARTx peripheral.
 *
 * @param   baudrate - USART communication baud rate.
 *
 * @return  None
 */
void USART_Printf_Init(uint32_t baudrate) {
    GPIO_InitTypeDef GPIO_InitStructure = { 0 };
    USART_InitTypeDef USART_InitStructure = { 0 };
    NVIC_InitTypeDef NVIC_InitStructure = { 0 };

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_USART1,
            ENABLE);

    /* USART1 TX-->D.5   RX-->D.6 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOD, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    USART_InitStructure.USART_BaudRate = baudrate;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl =
            USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;

    USART_Init(USART1, &USART_InitStructure);
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    ;

    USART_Cmd(USART1, ENABLE);
}

从该代码可以看出,其初始化串口的过程,总结出6个步骤。我们用标号①~⑥标
示了顺序:
① 串口时钟使能,GPIO 时钟使能
② 串口复位
③ GPIO 端口模式设置
④ 串口参数初始化
⑤ 初始化 NVIC 并且开启中断
⑥ 使能串口
这里需要重申的是,对于复用功能下的 GPIO 模式怎么判定,这个需要查看《CH32V003RM》P54 的表格“表 7-3 通用同步异步串行收发器(USART)”,这里还是拿出来再讲解一下吧。查看手册得知,配置全双工的串口 1,那么 TX(PA9)
管脚需要配置为推挽复用输出,RX(PA10)管脚配置为浮空输入或者带上拉输入。模式配置参考下面表格:
image

接下来,还要编写中断服务函数。串口 1 的中断服务函数USART1_IRQHandler

void USART1_IRQHandler(void) {
    u8 Res;
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        Res = USART_ReceiveData(USART1);
        if ((USART_RX_STA & 0x8000) == 0)       //接收未完成
                {
            if (USART_RX_STA & 0x4000)       //接收到了0x0d
                    {
                if (Res != 0x0a)
                    USART_RX_STA = 0;       //接收错误,重新开始
                else
                    USART_RX_STA |= 0x8000;  //接收完成了
            } else //还没收到0X0D
            {
                if (Res == 0x0d)
                    USART_RX_STA |= 0x4000;
                else {
                    USART_RX_BUF[USART_RX_STA & 0X3FFF] = Res;
                    USART_RX_STA++;
                    if (USART_RX_STA > (USART_REC_LEN - 1))
                        USART_RX_STA = 0; //接收数据错误,重新开始接收
                }
            }
        }
    }
}

大家对上面代码一定很熟悉,没错这就是正点原子STM32的串口接收代码,这里大家可以去支持正版学习一下;

3.3.3 下载验证:

打开串口调试助手可以看到,“请输入数据,以回车键结束”提示发送数据,勾选发送新行后,发送数据直接回显;
image

3.4 Eg4_EXTI

这一节,我们将向大家介绍如何使用 CH32V003的外部输入中断。在前面几节的学习中,我们掌握了 CH32V003的 IO 口最基本的操作。本节我们将介绍如何将 CH32V003的 IO 口作为外部中断输入;

这一节的代码主要分布在固件库的 ch32v00x_exti.h 和 ch32v00x_exti.c 文件中。
这里我们首先 CH32V003IO 口中断的一些基础概念。CH32V003的每个 IO 都可以作为外部中断的中断输入口,这点也是 CH32V003的强大之处。CH32V003的中断控制器支持9个外部中断/
事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。CH32V003的
9 个外部中断为:
线 0~7:Px0~Px7(x=A/C/D),任何一个 IO 口都可以启用外
部中断/事件功能,由 AFIO_EXTICRx 寄存器配置。
线 8:PVD 事件,超出电压监控阀值
线 9:自动唤醒事件。

3.4.1硬件设计:

本节配置PD0作为测试EXIT0的IO口;image

3.4.2 软件设计:

本节的代码设计,主要在我们的MyBSP目录下的EXIT.c中
先介绍 EXIT_Init函数,该函数代码如下:

void EXIT_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    EXTI_InitTypeDef EXTI_InitStructure = {0};
    NVIC_InitTypeDef NVIC_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOD, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    /* GPIOA ----> EXTI_Line0 */
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOD, GPIO_PinSource0);
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = EXTI7_0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

}

最后是中断请求函数声明和进入中断的处理

void EXTI7_0_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

/*********************************************************************
 * @fn      EXTI0_IRQHandler
 *
 * @brief   This function handles EXTI0 Handler.
 *
 * @return  none
 */
void EXTI7_0_IRQHandler(void)
{
  if(EXTI_GetITStatus(EXTI_Line0)!=RESET)
  {
    printf("Run at EXTI\r\n");
    EXTI_ClearITPendingBit(EXTI_Line0);     /* Clear Flag */
  }
}

这里打印提示进入了中断,也就是下降沿触发了,否则回到main函数的while循环;

3.4.3 下载验证:

打开串口调试助手可以看到,“Run at main”提示,拉低PD0后,“Run at EXTI”
image

3.5 Eg5_IWDG

这一节,我们将向大家介绍如何使用CH32V003 的独立看门狗(以下简称 IWDG)。CH32V003 内部自带了 2 个看门狗:独立看门狗(IWDG)和窗口看门狗(WWDG)。这一章我们只介绍独立看门狗,窗口看门狗将在下一节介绍。在本节中,我们将通过按键 PC1作为KEY1来喂狗,也是释放响应。

3.5.1硬件设计:

本节配置按键 PC1作为KEY1来喂狗。image

系统设有独立看门狗(IWDG)用来检测逻辑错误和外部环境干扰引起的软件故障。IWDG 时钟源来自于 LSI,可独立于主程序之外运行,适用于对精度要求低的场合。独立看门狗的时钟来源 LSI 时钟分频,其功能在停机和待机模式时仍能正常工作。当看门狗计数器自减到 0 时,将会产生系统复位,所以超时时间为(重装载值+1)个时钟。

3.5.2 软件设计:

本节的代码设计,主要在我们的MyBSP目录下的IWDG.c中
先介绍 IWDG_Feed_Init函数,该函数代码如下:

/*********************************************************************
 * @fn      IWDG_Init
 *
 * @brief   Initializes IWDG.
 *
 * @param   IWDG_Prescaler: specifies the IWDG Prescaler value.
 *            IWDG_Prescaler_4: IWDG prescaler set to 4.
 *            IWDG_Prescaler_8: IWDG prescaler set to 8.
 *            IWDG_Prescaler_16: IWDG prescaler set to 16.
 *            IWDG_Prescaler_32: IWDG prescaler set to 32.
 *            IWDG_Prescaler_64: IWDG prescaler set to 64.
 *            IWDG_Prescaler_128: IWDG prescaler set to 128.
 *            IWDG_Prescaler_256: IWDG prescaler set to 256.
 *          Reload: specifies the IWDG Reload value.
 *            This parameter must be a number between 0 and 0x0FFF.
 *
 * @return  none
 */
void IWDG_Feed_Init(u16 prer, u16 rlr)
{
    IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
    IWDG_SetPrescaler(prer);
    IWDG_SetReload(rlr);
    IWDG_ReloadCounter();
    IWDG_Enable();
}

IWDG_WriteAccessCmd操作的是IWDG 控制寄存器( IWDG_CTLR),描述如下:
image

我们可以跳转到定义,可以发现:IWDG_WriteAccessCmd,IWDG_ReloadCounter,IWDG_Enable;

以上三个函数均是操作IWDG_CTLR寄存器;

其中,

IWDG_WriteAccessCmd是0x5555,允许修改R16_IWDG_PSCR和R16_IWDG_RLDR 寄存器;

IWDG_ReloadCounter是0xAAAA,喂狗。加载 IWDG_RLDR 寄存器值到独立看门狗计数器中;

IWDG_Enable是0xCCCC:启动看门狗;

最后IWDG_SetPrescaler(prer)和IWDG_SetReload(rlr)分别设置的是分频因子寄存器( IWDG_PSCR )以及重装载值寄存器( IWDG_RLDR );

再有就是我们要实现的按下按键喂狗代码(释放喂狗):

void IWDG_Handle(void)
{
    static uint8_t bounce=0;
    if(KEY1)
    {
        bounce=1;
    }else{
        if(bounce)
        {
            bounce=0;
            printf("Feed dog\r\n");
            IWDG_ReloadCounter();   //Feed dog
            Delay_Ms(10);
        }
    }

}

3.5.3 下载验证:

可以看到每隔4S左右复位,喂狗可以延迟复位时间;

image

3.6 Eg6_WWDG

窗口看门狗一般用来监测系统运行的软件故障,例如外部干扰、不可预见的逻辑错误等情况。它需要在一个特定的窗口时间(有上下限)内进行计数器刷新(喂狗),否则早于或者晚于这个窗口时间看门狗电路都会产生系统复位。

3.6.1硬件设计:

窗口看门狗运行基于一个 7 位的递减计数器,其挂载在 APB1 总线下,计数时基 WWDG_CLK 来源(PCLK1/4096)时钟的分频,分频系数在配置寄存器 WWDG_CFGR 中的 WDGTB[1:0]域设置。递减计数器处于自由运行状态,无论看门狗功能是否开启,计数器一直循环递减计数。如图 5-1 所示,窗口看门狗内部结构框图。
image

  • 启动窗口看门狗

      系统复位后,看门狗处于关闭状态,设置 WWDG_CTLR 寄存器的 WDGA 位能够开启看门狗,随后它不能再被关闭,除非发生复位。注:可以通过设置 RCC_APB1PCENR 寄存器关闭 WWDG 的时钟来源,暂停 WWDG_CLK 计数,间接停止看门狗功能,或者通过设置 RCC_APB1PRSTR 寄存器复位 WWDG 模块,等效为复位的作用。
    
  • 看门狗配置

看门狗内部是一个不断循环递减运行的 7 位计数器,支持读写访问。使用看门狗复位功能,需要
执行下面几点操作:

	1) 计数时基:通过 WWDG_CFGR 寄存器的 WDGTB[1:0]位域,注意要开启 RCC 单元的 WWDG 模块时钟。
	2) 窗口计数器:设置 WWDG_CFGR 寄存器的 W[6:0]位域,此计数器由硬件用作和当前计数器比较使用,数值由用户软件配置,不会改变。作为窗口时间的上限值。

	3) 看门狗使能:WWDG_CTLR 寄存器 WDGA 位软件置 1,开启看门狗功能,可以系统复位。
	4) 喂狗:即刷新当前计数器值,配置 WWDG_CTLR 寄存器的 T[6:0]位域。此动作需要在看门狗功能

开启后,在周期性的窗口时间内执行,否则会出现看门狗复位动作。

  • 喂狗窗口时间

    如图 5-2 所示,灰色区域为窗口看门狗的监测窗口区域,其上限时间 t2 对应当前计数器值达到窗口值W[6:0]的时间点;其下限时间t3 对应当前计数器值达到0x3F的时间点。此区域时间内 t2<t<t3可以进行喂狗操作(写 T[6:0]),刷新当前计数器的数值。
    image

  • 看门狗复位:

        1) 当没有及时喂狗操作,导致 T[6:0]计数器的值由 0x40 变成 0x3F,将出现“窗口看门狗复位”,
    

产生系统复位。即 T6-bit 被硬件检测为 0,将出现系统复位。
注:应用程序可以通过软件写 T6-bit 为 0,实现系统复位,等效软件复位功能。
2) 当在不允许喂狗时间内执行计数器刷新动作,即在 t1≤t≤t2 时间内操作写 T[6:0]位域,将出
现“窗口看门狗复位”,产生系统复位。

  • 提前唤醒

       为了防止没有及时刷新计数器导致系统复位,看门狗模块提供了早期唤醒中断(EWI)通知。当
    

计数器自减到 0x40 时,产生提前唤醒信号,WEIF 标志置 1,如果置位了 EWI 位,会同时触发窗口看
门狗中断。此时距离硬件复位有 1 个计数器时钟周期(自减为 0x3F),应用程序可在此时间内即时
进行喂狗操作。

3.6.2 软件设计:

先介绍 WWDG_Config函数,该函数代码如下:

/*********************************************************************
 * @fn      WWDG_Config
 *
 * @brief   Configure WWDG.
 *
 * @param   tr - The value of the decrement counter(0x7f~0x40)
 *          wr - Window value(0x7f~0x40)
 *          prv - Prescaler value
 *            WWDG_Prescaler_1
 *            WWDG_Prescaler_2
 *            WWDG_Prescaler_4
 *            WWDG_Prescaler_8
 *
 * @return  none
 */
void WWDG_Config(uint8_t tr, uint8_t wr, uint32_t prv)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);
    WWDG_SetCounter(tr);
    WWDG_SetPrescaler(prv);
    WWDG_SetWindowValue(wr);
    WWDG_Enable(WWDG_CNT);
    WWDG_ClearFlag();
    WWDG_NVIC_Config();
    WWDG_EnableIT();
}

其中WWDG_SetCounter(tr)与WWDG_Enable(WWDG_CNT;都是对WWDG 控制寄存器( WWDG_CTLR )进行操作,以下是WWDG 控制寄存器的描述
image

WWDG_Config(0x7f, 0x5f, WWDG_Prescaler_8);

我们在主函数中调用如上,WWDG_SetCounter(tr)是第一个参数0x7F,也就是设置T[6:0]=0x7F;

再看WWDG_Enable如下:

#define CTLR_WDGA_Set      ((uint32_t)0x00000080)
#define WWDG_CNT    0X7F
void WWDG_Enable(uint8_t Counter)
{
    WWDG->CTLR = CTLR_WDGA_Set | Counter;
}

由此,我们可任意看到WWDG_Enable(WWDG_CNT)就是WWDG_CTLR的bit7置位开启看门狗;

在介绍WWDG_SetPrescaler、WWDG_SetWindowValue与WWDG_EnableIT函数之前,我们先看WWDG 配置寄存器( WWDG_CFGR )
image

)

然后大家跳转到定义就可以发现,以上三个函数,实际上就是操作WWDG_CFGR 寄存器;

最后是WWDG_ClearFlag是操作WWDG 状态寄存器( WWDG_STATR )
image

WWDG_NVIC_Config是中断优先级的配置;

最后我们在main函数中喂狗

int main(void)
{
    u8 wwdg_tr, wwdg_wr;

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    Delay_Init();
    USART_Printf_Init(115200);
    printf("SystemClk:%d\r\n", SystemCoreClock);

    printf("WWDG Test\r\n");
    WWDG_Config(0x7f, 0x5f, WWDG_Prescaler_8); /* 48M/8/4096 */
    wwdg_wr = WWDG->CFGR & 0x7F;
    while(1)
    {
        Delay_Ms(50);

        printf("**********\r\n");
        wwdg_tr = WWDG->CTLR & 0x7F;
        if(wwdg_tr < wwdg_wr)
        {
            WWDG_Feed();
        }
        printf("##########\r\n");
    }
}

由于我们时间上设置的是50ms喂一次,超出了不在窗口时间内,故而会复位,我们减少喂狗时间在窗口期内,系统不会复位;

3.6.3 下载验证:

Delay_Ms(50)的实验现象
image

Delay_Ms(25)的实验现象
image

3.7 Eg7_TIMRT

这一节,我们将向大家介绍如何使用 ch32v003的通用定时器,ch32v003 的定时器功能十分强大,有 TIME1 高级定时器,也有 TIME2通用定时器,在本节中,我们将利用 TIM2 的定时器中断来控制LED1 的翻转,在主函数用 LED2的翻转来提示程序正在运行。本节,我们选择难度适中的通用定时器来介绍。

3.7.1硬件设计:

通用定时器模块包含一个 16 位可自动重装的定时器 TIM2,用于测量脉冲宽度或者产生特定频率的脉冲、PWM 波等。可用于自动化控制、电源等领域。

  • 通用定时器的主要特征包括:
    • 16 位自动重装计数器,支持增计数模式,减计数模式和增减计数模式;
    • 16 位预分频器,分频系数从 1~65536 之间动态可调;
    • 支持四路独立的比较捕获通道;
    • 每路比较捕获通道支持多种工作模式,比如:输入捕获、输出比较、PWM 生成和单脉冲输出;
    • 支持外部信号控制定时器;
    • 支持在多种模式下使用 DMA;
    • 支持在多种模式下使用 DMA;
    • 支持增量式编码,定时器之间的级联和同步;

原理和结构

image

由于篇幅的原因,大家自行查阅参考手册;

3.7.2 软件设计:

先介绍 TIM2_Int_Init函数,arr和psc分别是周期值和预分频器值;

void TIM2_Int_Init(u16 arr, u16 psc)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能TIM2时钟

    TIM_TimeBaseStructure.TIM_Period = arr;   //指定下次更新事件时要加载到活动自动重新加载寄存器中的周期值。
    TIM_TimeBaseStructure.TIM_Prescaler = psc; //指定用于划分TIM时钟的预分频器值。
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频因子
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM计数模式,向上计数模式
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //使能TIM2中断,允许更新中断

    //初始化TIM NVIC,设置中断优先级分组
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;           //TIM2中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //设置抢占优先级0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;        //设置响应优先级1
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;           //使能通道1中断
    NVIC_Init(&NVIC_InitStructure); //初始化NVIC

    TIM_Cmd(TIM2, ENABLE); //TIM2使能
}

我们打开TIM_TimeBaseInitTypeDef可以看到,这个结构体实际上是管理了TIM_Prescaler预分频值,TIM_CounterMode计数器模式,TIM_Period自动重新加载寄存器中的周期值,TIM_ClockDivision时钟分频因子,TIM_RepetitionCounter重复计数器值;

/* TIM Time Base Init structure definition */
typedef struct
{
    uint16_t TIM_Prescaler; /* Specifies the prescaler value used to divide the TIM clock.
                               This parameter can be a number between 0x0000 and 0xFFFF */

    uint16_t TIM_CounterMode; /* Specifies the counter mode.
                                 This parameter can be a value of @ref TIM_Counter_Mode */

    uint16_t TIM_Period; /* Specifies the period value to be loaded into the active
                            Auto-Reload Register at the next update event.
                            This parameter must be a number between 0x0000 and 0xFFFF.  */

    uint16_t TIM_ClockDivision; /* Specifies the clock division.
                                  This parameter can be a value of @ref TIM_Clock_Division_CKD */

    uint8_t TIM_RepetitionCounter; /* Specifies the repetition counter value. Each time the RCR downcounter
                                      reaches zero, an update event is generated and counting restarts
                                      from the RCR value (N).
                                      This means in PWM mode that (N+1) corresponds to:
                                         - the number of PWM periods in edge-aligned mode
                                         - the number of half PWM period in center-aligned mode
                                      This parameter must be a number between 0x00 and 0xFF.
                                      @note This parameter is valid only for TIM1 and TIM8. */
} TIM_TimeBaseInitTypeDef;

我们实际上只需要对这些参数进行配置,即可通过TIM_TimeBaseInit这个函数去配置控制寄存器 1 (TIM2_CTLR1 ),自动 重装 值寄存器( TIM2_ATRLR ), 计数时钟预分频器( TIM2_PSC ),大家跳转到TIM_TimeBaseInit即可看到操作这三个寄存器的过程,如下

/*********************************************************************
 * @fn      TIM_TimeBaseInit
 *
 * @brief   Initializes the TIMx Time Base Unit peripheral according to
 *        the specified parameters in the TIM_TimeBaseInitStruct.
 *
 * @param   TIMx - where x can be 1 to 2 to select the TIM peripheral.
 *          TIM_TimeBaseInitStruct - pointer to a TIM_TimeBaseInitTypeDef
 *        structure.
 *
 * @return  none
 */
void TIM_TimeBaseInit(TIM_TypeDef *TIMx, TIM_TimeBaseInitTypeDef *TIM_TimeBaseInitStruct)
{
    uint16_t tmpcr1 = 0;

    tmpcr1 = TIMx->CTLR1;

    if((TIMx == TIM1) || (TIMx == TIM2))
    {
        tmpcr1 &= (uint16_t)(~((uint16_t)(TIM_DIR | TIM_CMS)));
        tmpcr1 |= (uint32_t)TIM_TimeBaseInitStruct->TIM_CounterMode;
    }

    tmpcr1 &= (uint16_t)(~((uint16_t)TIM_CTLR1_CKD));
    tmpcr1 |= (uint32_t)TIM_TimeBaseInitStruct->TIM_ClockDivision;

    TIMx->CTLR1 = tmpcr1;
    TIMx->ATRLR = TIM_TimeBaseInitStruct->TIM_Period;
    TIMx->PSC = TIM_TimeBaseInitStruct->TIM_Prescaler;

    if(TIMx == TIM1)
    {
        TIMx->RPTCR = TIM_TimeBaseInitStruct->TIM_RepetitionCounter;
    }

    TIMx->SWEVGR = TIM_PSCReloadMode_Immediate;
}

TIM_ITConfig这个函数是配置DMA/ 中 断使能 寄存器( TIM2_DMAINTENR)这个寄存器,大家可以打开参考手册11.4.4,可以看到这个寄存器的描述,TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); 使能TIM2中断,允许更新中断实际上就是把TIM2_DMAINTENR第0位置位;

由于用到了中断,需要配置一下NVIC_InitStructure,然后使能定时器2即可TIM_Cmd(TIM2, ENABLE)就初始完成了;

最后是声明TIM2的中断请求函数,并实现它,我们在里面翻转LED2;

void TIM2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void TIM2_IRQHandler(void)
{
    static u8 i = 0;
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) //检查TIM2中断是否发生。
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);    //清除TIM3的中断挂起位。
        printf("Enter interrupt\n");
        GPIO_WriteBit(GPIOC, GPIO_Pin_3, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET));
    }
}

最后是在main函数的while循环中调用LED_Handle以提示程序正在允许

void LED_Handle(void)
{
    LED2_ON();
    Delay_Ms(100);
    LED2_OFF();
    Delay_Ms(100);
}

3.7.3 下载验证:

可以看到LED2每100ms翻转一次,LED1每500ms翻转一次。

3.8 Eg8_PWM

这一节,我们将向大家介绍PWM,PWM 输出模式是定时器的基本功能之一。PWM 输出模式最常见的是使用重装值确定 PWM 频率,使用捕获比较寄存器确定占空比的方法。将 OCxM 域中置 110b 或者 111b 使用 PWM 模式 1 或者模式 2,置 OCxPE 位使能预装载寄存器,最后置 ARPE位使能预装载寄存器的自动重装载。在发生一个更新事件时,预装载寄存器的值才能被送到影子寄存器,所以在核心计数器开始计数之前,需要置 UG 位来初始化所有寄存器。在 PWM 模式下,核心计数器和比较捕获寄存器一直在进行比较,根据 CMS 位,定时器能够输出边沿对齐或者中央对齐的 PWM 信号。

  • 边沿对齐
    使用边沿对齐时,核心计数器增计数或者减计数,在 PWM 模式 1 的情景下,在核心计数器的值大于比较捕获寄存器时,OCxREF 上升为高;当核心计数器的值小于比较捕获寄存器时(比如核心计数器增长到 R16_TIMx_ATRLR 的值而恢复成全 0 时),OCxREF 下降为低。
  • 中央对齐
    使用中央对齐模式时,核心计数器运行在增计数和减计数交替进行的模式下,OCxREF 在核心计数器和比较捕获寄存器的值一致时进行上升和下降的跳变。但比较标志在三种中央对齐模式下,置位的时机有所不同。在使用中央对齐模式时,最好在启动核心计数器之前产生一个软件更新标志(置UG 位)。

3.8.1硬件设计:

我们将使用PD3作为T2CH2通道去输出PWM信号控制LED;

3.8.2 软件设计:

先介绍 TIM2_PWMOut_Init函数,arr和psc分别是周期值和预分频器值,ccp是要加载到捕获比较寄存器中的脉冲值;

void TIM2_PWMOut_Init( u16 arr, u16 psc, u16 ccp )
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;

    RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOD , ENABLE );//使能GPIOA外设时钟和TIM1时钟
    RCC_APB1PeriphClockCmd( RCC_APB1Periph_TIM2 ,ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;        //配置PD3引脚
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //设置为复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//设置输出速度:50MHz
    GPIO_Init( GPIOD, &GPIO_InitStructure );         //GPIO初始化

    TIM_TimeBaseInitStructure.TIM_Period = arr;      //指定下次更新事件时要加载到活动自动重新加载寄存器中的周期值。
    TIM_TimeBaseInitStructure.TIM_Prescaler = psc;   //指定用于划分TIM时钟的预分频器值。
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;    //时钟分频因子
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//TIM计数模式,向上计数模式
    TIM_TimeBaseInit( TIM2, &TIM_TimeBaseInitStructure); //根据指定的参数初始化TIMx的时间基数单位

    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;            //指定TIM模式
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//指定TIM输出比较状态,即使能比较输出
    TIM_OCInitStructure.TIM_Pulse = ccp;                         //指定要加载到捕获比较寄存器中的脉冲值。
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;    //指定输出极性。
    TIM_OC2Init( TIM2, &TIM_OCInitStructure ); //根据TIM_OCInitStruct中指定的参数初始化TIM2 Channel2。

    TIM_CtrlPWMOutputs(TIM2, ENABLE );                  //启用定时器1PWM输出
    TIM_OC2PreloadConfig( TIM2, TIM_OCPreload_Disable );//使能CCR1上的TIM1外设预加载寄存器
    TIM_ARRPreloadConfig( TIM2, ENABLE );               //使能ARR上TIM1外设预加载寄存器
    TIM_Cmd( TIM2, ENABLE );                            //使能TIM1

}

GPIO_InitTypeDef和TIM_TimeBaseInitTypeDef在前面的章节已经介绍过,我们现在来介绍一下TIM_OCInitTypeDef,根据注释可以知道TIM_OCInitTypeDef实际上是TIM输出比较结构体的定义,

/* TIM Output Compare Init structure definition */
typedef struct
{
    uint16_t TIM_OCMode; /* Specifies the TIM mode.
                            This parameter can be a value of @ref TIM_Output_Compare_and_PWM_modes */

    uint16_t TIM_OutputState; /* Specifies the TIM Output Compare state.
                                 This parameter can be a value of @ref TIM_Output_Compare_state */

    uint16_t TIM_OutputNState; /* Specifies the TIM complementary Output Compare state.
                                  This parameter can be a value of @ref TIM_Output_Compare_N_state
                                  @note This parameter is valid only for TIM1 and TIM8. */

    uint16_t TIM_Pulse; /* Specifies the pulse value to be loaded into the Capture Compare Register.
                           This parameter can be a number between 0x0000 and 0xFFFF */

    uint16_t TIM_OCPolarity; /* Specifies the output polarity.
                                This parameter can be a value of @ref TIM_Output_Compare_Polarity */

    uint16_t TIM_OCNPolarity; /* Specifies the complementary output polarity.
                                 This parameter can be a value of @ref TIM_Output_Compare_N_Polarity
                                 @note This parameter is valid only for TIM1 and TIM8. */

    uint16_t TIM_OCIdleState; /* Specifies the TIM Output Compare pin state during Idle state.
                                 This parameter can be a value of @ref TIM_Output_Compare_Idle_State
                                 @note This parameter is valid only for TIM1 and TIM8. */

    uint16_t TIM_OCNIdleState; /* Specifies the TIM Output Compare pin state during Idle state.
                                  This parameter can be a value of @ref TIM_Output_Compare_N_Idle_State
                                  @note This parameter is valid only for TIM1 and TIM8. */
} TIM_OCInitTypeDef;

其中,TIM_OCMode是指定TIM模式,我们这里配置为TIM_OCMode_PWM2,这是PWM2模式,与之相对的是PWM1模式,

区别如下:

  • PWM1模式:递增计数时,当TIMx_CNT(计数值)< TIMx_CCR(捕获/比较值)时,输出为有效电平,否则为无效电平。递减计数模式则刚好相反。
  • PWM2模式:递增计数时,当TIMx_CNT(计数值)< TIMx_CCR(捕获/比较值)时,输出为无效电平,否则为有效电平。递减计数模式则刚好相反。

TIM_OutputState是指定TIM输出比较状态,即使能比较输出,TIM_OutputNState不用配置,因为这是高级定时器的内容;

TIM_Pulse是指定要加载到捕获比较寄存器中的脉冲值。TIM_OCPolarity是指定输出极性;之后使能输出即可。

在main函数的while循环中改变PWM以控制LED;

int main(void)
{

    u16 led0pwmval=0;
    u8 dir=1;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    Delay_Init();
    USART_Printf_Init(115200);
    printf("SystemClk:%d\r\n", SystemCoreClock);

    TIM2_PWMOut_Init( 599, 1, 300 );

    while(1)
    {
        Delay_Ms(10);
        if(dir)
            led0pwmval++;
        else
            led0pwmval--;

        if(led0pwmval>300)
            dir=0;
        if(led0pwmval==0)
            dir=1;
        TIM_SetCompare2(TIM2,led0pwmval);

    }
}

3.8.3 下载验证:

可以看到LED1变亮后又慢慢变暗;

3.9 Eg9_ICAPTURE

​ 输入捕获模式是定时器的基本功能之一。输入捕获模式的原理是,当检测到 ICxPS 信号上确定的边沿后,则产生捕获事件,计数器当前的值会被锁存到比较捕获寄存器(R16_TIMx_CHCTLRx)中。发生捕获事件时,CCxIF(在 R16_TIMx_INTFR 中)被置位,如果使能了中断或者 DMA,还会产生相应中断或者 DMA。如果发生捕获事件时,CCxIF 已经被置位了,那么 CCxOF 位会被置位。CCxIF 可由软件清除,也可以通过读取比较捕获寄存器由硬件清除。CCxOF 由软件清除。举个通道 1 的例子来说明使用输入捕获模式的步骤,如下:
1) 配置 CCxS 域,选择 ICx 信号的来源。比如设为 10b,选择 TI1FP1 作为 IC1 的来源,不可以使用默认设置,CCxS 域默认是使比较捕获模块作为输出通道;
2) 配置 ICxF 域,设定 TI 信号的数字滤波器。数字滤波器会以确定的频率,采样确定的次数,再输出一个跳变。这个采样频率和次数是通过 ICxF 来确定的;
3) 配置 CCxP 位,设定 TIxFPx 的极性。比如保持 CC1P 位为低,选择上升沿跳变;
4) 配置 ICxPS 域,设定 ICx 信号成为 ICxPS 之间的分频系数。比如保持 ICxPS 为 00b,不分频;
5) 配置 CCxE 位,允许捕获核心计数器(CNT)的值到比较捕获寄存器中。置 CC1E 位;
6) 根据需要配置 CCxIE 和 CCxDE 位,决定是否允许使能中断或者 DMA。
至此已经将比较捕获通道配置完成。
当 TI1 输入了一个被捕获的脉冲时,核心计数器(CNT)的值会被记录到比较捕获寄存器中,CC1IF被置位,当 CC1IF 在之前就已经被置位时,CCIOF 位也会被置位。如果 CC1IE 位,那么会产生一个中断;如果 CC1DE 被置位,会产生一个 DMA 请求。可以通过写事件产生寄存器的方式(R16_TIMx_SWEVGR)的方式由软件产生一个输入捕获事件。

3.9.1硬件设计:

我们将使用PD3作为T2CH2通道去捕获高电平时间。

3.9.2 软件设计:

先介绍 TIM2_ICapture_Init函数,arr和psc分别是周期值和预分频器值;我们配置PD3作为T2CH2捕获高电平时间,开启更新中断和通道2输入捕获中断。

void TIM2_ICapture_Init(u16 arr, u16 psc)
{
    GPIO_InitTypeDef        GPIO_InitStructure = {0};
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure = {0};
    NVIC_InitTypeDef        NVIC_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能TIM2时钟

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_Init(GPIOD, &GPIO_InitStructure);
    GPIO_ResetBits(GPIOD, GPIO_Pin_3);

    TIM_TimeBaseInitStructure.TIM_Period = arr;
    TIM_TimeBaseInitStructure.TIM_Prescaler = psc;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0x00;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInit(TIM2,&TIM_ICInitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    TIM_ITConfig(TIM2, TIM_IT_Update | TIM_IT_CC2, ENABLE);
    TIM_Cmd(TIM2, ENABLE);
}

TIM2_ICapture_Init( 0xFFFF, 47 );

下面是中断回调函数,TIM2CH2_CAPTURE_STA各个为如下

  • bit7:捕获完成标志;
  • bit6:捕获到高电平标志;
  • bit5~0:捕获到高电平后定时器溢出的次数;

TIM2CH2_CAPTURE_VAL是输入捕获值。

//TIM2CH2_CAPTURE_STA
//bit7:捕获完成标志
//bit6:捕获到高电平标志
//bit5~0:捕获到高电平后定时器溢出的次数
u8  TIM2CH2_CAPTURE_STA=0;  //输入捕获状态
u16 TIM2CH2_CAPTURE_VAL;    //输入捕获值,用来记录捕获到下降沿的时候,TIM2_CNT的值

//定时器2中断服务程序
void TIM2_IRQHandler(void)
{
    if((TIM2CH2_CAPTURE_STA&0X80)==0)//还未成功捕获
    {
        if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
        {
            if(TIM2CH2_CAPTURE_STA&0X40)//已经捕获到高电平了
            {
                if((TIM2CH2_CAPTURE_STA&0X3F)==0X3F)//高电平太长了
                {
                    TIM2CH2_CAPTURE_STA|=0X80;//标记成功捕获了一次
                    TIM2CH2_CAPTURE_VAL=0XFFFF;
                }else TIM2CH2_CAPTURE_STA++;
            }
        }
    if (TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET)//捕获2发生捕获事件
        {
            if(TIM2CH2_CAPTURE_STA&0X40)        //捕获到一个下降沿
            {
                TIM2CH2_CAPTURE_STA|=0X80;      //标记成功捕获到一次高电平脉宽
                TIM2CH2_CAPTURE_VAL=TIM_GetCapture2(TIM2);
                TIM_OC2PolarityConfig(TIM2,TIM_ICPolarity_Rising); //CC2P=0 设置为上升沿捕获
            }else                               //还未开始,第一次捕获上升沿
            {
                TIM2CH2_CAPTURE_STA=0;          //清空
                TIM2CH2_CAPTURE_VAL=0;
                TIM_SetCounter(TIM2,0);
                TIM2CH2_CAPTURE_STA|=0X40;      //标记捕获到了上升沿
                TIM_OC2PolarityConfig(TIM2,TIM_ICPolarity_Falling);     //CC1P=1 设置为下降沿捕获
            }
        }
    }
    TIM_ClearITPendingBit(TIM2, TIM_IT_CC2|TIM_IT_Update); //清除中断标志位
}

3.9.3 下载验证:

可以看到捕获到高电平的值:

image

3.10 Eg10_ADC

CH32V003的 ADC 是 10 位逐次逼近型的模拟数字转换器。

3.10.1硬件设计:

我们将使用PC4作为模拟输入通道2,采集外部电压值。

3.10.2 软件设计:

先介绍 ADC_Function_Init函数,主要是对ADC_InitStructure和GPIO_InitStructure进行初始化,PC4配置为模拟输入模式,ADC1配置为独立模式,禁止扫描模式,使能连续采样模式,数据右对齐。配置完成后需要等待校正完成才开始采样。

void ADC_Function_Init(void)
{
    ADC_InitTypeDef  ADC_InitStructure = {0};
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div8);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    ADC_DeInit(ADC1);
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_Calibration_Vol(ADC1, ADC_CALVOL_50PERCENT);
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

最后是获取ADC值并求平均。

//获得ADC值
u16 Get_Adc(u8 ch)
{
    //设置指定ADC的规则组通道,一个序列,采样时间
    ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_241Cycles );  //ADC1,ADC通道,采样时间为239.5周期

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);     //使能指定的ADC1的软件转换启动功能

    while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束

    return ADC_GetConversionValue(ADC1);    //返回最近一次ADC1规则组的转换结果
}

u16 Get_Adc_Average(u8 ch,u8 times)
{
    u32 temp_val=0;
    u8 t;
    for(t=0;t<times;t++)
    {
        temp_val+=Get_Adc(ch);
        Delay_Ms(5);
    }
    return temp_val/times;
}


int main(void)
{

    u16 temp=0;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    Delay_Init();
    USART_Printf_Init(115200);
    printf("SystemClk:%d\r\n", SystemCoreClock);
    ADC_Function_Init();
    while(1)
    {
        Delay_Ms(10);
        temp=Get_Adc_Average(2,10);
        printf("ADC Value:%d\r\n",temp);
    }
}

3.10.3 下载验证:

可以看到最值求得的平均值如下:

image-20221121211228105

3.11 Eg11_Flash_EEPROM

今天要实现的是CH32V003的Flash模拟EEPROM

3.11.1硬件设计:

  1. 闪存组织:
    image

上述主存储器区域用于用户的应用程序存储,以 1K 字节(16 页)单位进行写保护划分;除
了“厂商配置字”区域出厂锁定,用户不可访问,其他区域在一定条件下用户可操作。

  1. 闪存编程:
  • 标准编程:此方式是默认编程方式(兼容方式)。这种模式下 CPU 以单次 2 字节方式执行编程,
    单次 1K 字节执行擦除及整片擦除操作。
  • 快速编程:此方式采用页操作方式(推荐)。经过特定序列解锁后,执行单次 64 字节的编程及
    64 字节擦除、1K 字节擦除及整片擦除。

3.11.2 软件设计:

  1. STMFLASH_Write写Flash函数的移植,这里我们直接复制粘贴。
#define STM32_FLASH_SIZE 16             //所选STM32的FLASH容量大小(单位为K)
#define STM32_FLASH_BASE 0x08000000     //STM32 FLASH的起始地址
#define STM_SECTOR_SIZE 64 //字节
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是32*2字节
//不检查的写入
//WriteAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
    u16 i;
    for(i=0;i<NumToWrite;i++)
    {
        FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);
        WriteAddr+=2;//地址增加2.
    }
}
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
    u32 secpos;    //扇区地址
    u16 secoff;    //扇区内偏移地址(16位字计算)
    u16 secremain; //扇区内剩余地址(16位字计算)
    u16 i;
    u32 offaddr;   //去掉0X08000000后的地址
    if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
    FLASH_Unlock();                     //解锁
    offaddr=WriteAddr-STM32_FLASH_BASE;     //实际偏移地址.
    secpos=offaddr/STM_SECTOR_SIZE;         //扇区地址  0~127 for STM32F103RBT6
    secoff=(offaddr%STM_SECTOR_SIZE)/2;     //在扇区内的偏移(2个字节为基本单位.)
    secremain=STM_SECTOR_SIZE/2-secoff;     //扇区剩余空间大小
    if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
    while(1)
    {
        STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
        for(i=0;i<secremain;i++)//校验数据
        {
            if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除
        }
        if(i<secremain)//需要擦除
        {
            FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
            for(i=0;i<secremain;i++)//复制
            {
                STMFLASH_BUF[i+secoff]=pBuffer[i];
            }
            STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区
        }else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间.
        if(NumToWrite==secremain)break;//写入结束了
        else//写入未结束
        {
            secpos++;               //扇区地址增1
            secoff=0;               //偏移位置为0
            pBuffer+=secremain;     //指针偏移
            WriteAddr+=(secremain*2);   //写地址偏移
            NumToWrite-=secremain;  //字节(16位)数递减
            if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
            else secremain=NumToWrite;//下一个扇区可以写完了
        }
    };
    FLASH_Lock();//上锁
}
  1. STMFLASH_Read读Flash函数的移植,这里我们直接复制粘贴。
//读取指定地址的半字(16位数据)
//faddr:读地址(此地址必须为2的倍数!!)
//返回值:对应数据.
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
    return *(vu16*)faddr;
}

//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)
{
    u16 i;
    for(i=0;i<NumToRead;i++)
    {
        pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
        ReadAddr+=2;//偏移2个字节.
    }
}
  1. 下载验证

    最后在main函数中写个测试程序:

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    Delay_Init();
    USART_Printf_Init(115200);
    Delay_Ms(1000);
    u16 Temp=0xA5;
    STMFLASH_Write(LAST_PAGE_ADDR,(u16*)&Temp,1);
    printf("Temp1=%x\r\n",Temp);
    Temp=0;
    STMFLASH_Read(LAST_PAGE_ADDR,(u16*)&Temp,1);
    printf("Temp2=%x\r\n",Temp);
    Temp=0x5A;
    STMFLASH_Write(LAST_PAGE_ADDR+2,(u16*)&Temp,1);
    printf("Temp1=%x\r\n",Temp);
    Temp=0;
    STMFLASH_Read(LAST_PAGE_ADDR+2,(u16*)&Temp,1);
    printf("Temp2=%x\r\n",Temp);
    while(1)
    {
    }
}

3.11.3 下载验证:

可以看到读出和写入的值一致

image

第四部分、电子模块篇——提升实例

4.1 Eg1_WS2812

本节实例是驱动WS2812B圆环模块,实现rainbowCycle。

4.1.1硬件设计:

PWM输出就是对外输出脉宽(即占空比)可调的方波信号,信号频率由自动重装载寄存器ARR的值决定,占空比由比较寄存器CCR的值决定。

由WS2812规格书可知:

  1. WS2812灯珠的通信速度为800Kbit/s。

  2. 数据传输时间(TH+TL=1.25us+/-600ns)

    T0H 0码,高电平时间 0.35us +/-150ns
    T1H 1码,高电平时间 0.7us +/-150ns
    T0L 0码,低电平时间 0.8us +/-150ns
    T1L 1码,低电平时间 0.6us +/-150ns
    RES 帧单位,低电平时间 50us以上

故在配置CH32V003定时器时可设置,预分频因子为0,自动重装载初值为60,则频率为48M/60 = 800K。

设1码值占空比为35可满足1码的时间要求,设值占空比为20可满足0码的时间要求。

这里在代码中具体用到CH32V003的PD3引脚(TIM2的CH2)。

4.1.2软件设计:

九层之台。始于垒土。要想实现炫丽的彩灯效果,首先要做的事情就在于配置好外设,我们采用的是PWM+DMA的方法驱动WS2812B,因为PWM可以很好的契合WS2812B的时序要求。而DMA可以在输出信号时候使其不受干扰。

我们先来看main函数。

int main(void)
{
    USART_Printf_Init(115200);
    printf("SystemClk:%d\r\n", SystemCoreClock);
    Delay_Init();
    WS_WriteAll_RGB(0x0,0,0);
    TIM2_PWMOut_Init(60, 0, 0);
    TIM2_DMA_Init(DMA1_Channel2, (u32)TIM2_CH2CVR_ADDRESS, (u32)send_Buf, NUM);
    TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE);
    TIM_Cmd(TIM2, ENABLE);
    TIM_CtrlPWMOutputs(TIM2, ENABLE);
    while(1)
    {
        ws281x_rainbowCycle(10);
    }
}

WS_WriteAll_RGB是设置所有灯的颜色,传入的参数的是RGB,三基色。

TIM2_PWMOut_Init的参数如下 ,arr 是周期值,psc 是预分频值,ccp 是脉冲值或是捕获比较器值

  • arr - the period value.

  •      psc - the prescaler value.
    
  •      ccp - the pulse value.
    

我们现在就来看看这个函数的实现

void TIM2_PWMOut_Init(u16 arr, u16 psc, u16 ccp)
{
    GPIO_InitTypeDef        GPIO_InitStructure = {0};
    TIM_OCInitTypeDef       TIM_OCInitStructure = {0};
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD , ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    TIM_TimeBaseInitStructure.TIM_Period = arr;
    TIM_TimeBaseInitStructure.TIM_Prescaler = psc;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = ccp;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC2Init(TIM2, &TIM_OCInitStructure);

    TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Disable);
    TIM_ARRPreloadConfig(TIM2, ENABLE);
}

我们实例化GPIO_InitStructure,配置PD3为推完复用模式,实例化TIM_TimeBaseInitTypeDef,配置WS2812所需要的800KHz,实例化TIM_OCInitTypeDef,配置为PWM1模式。

接着TIM2_DMA_Init则是配置TIM2的DMA,

void TIM2_DMA_Init(DMA_Channel_TypeDef *DMA_CHx, u32 ppadr, u32 memadr, u16 bufsize)
{
    DMA_InitTypeDef DMA_InitStructure = {0};

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    DMA_DeInit(DMA_CHx);
    DMA_InitStructure.DMA_PeripheralBaseAddr = ppadr;
    DMA_InitStructure.DMA_MemoryBaseAddr = memadr;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_BufferSize = bufsize;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA_CHx, &DMA_InitStructure);

    DMA_Cmd(DMA_CHx, ENABLE);
}

TIM2_DMA_Init(DMA1_Channel2, (u32)TIM2_CH2CVR_ADDRESS, (u32)send_Buf, NUM);,由下图可知TIM2_UP是DMA的通道2,故而选择DMA1_Channel2,TIM2 比较/捕获寄存器 2(TIM2_CH2CVR_ADDRESS )地址是0x40000038,然后再传入缓存send_Buf和大小NUM即可。

image

最后,使能TIM2 DMA,使能TIM2 ,使能TIM2 PWM输出。

	TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE);
    TIM_Cmd(TIM2, ENABLE);
    TIM_CtrlPWMOutputs(TIM2, ENABLE)

接下来我们就要实现rainbowCycle彩虹效果,

void ws281x_rainbowCycle(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
    for(i=0; i< PIXEL_NUM; i++) {
       WS281x_SetPixelColor(i,Wheel(((i * 256 / PIXEL_NUM) + j) & 255));

    }
    ws281x_show();
    Delay_Ms(10);
  }
}

其中Wheel是输入0~255,然后输出RGB的颜色值,这个七彩渐变算法;而WS281x_SetPixelColor则是设置第n颗灯珠的GRB值。ws281x_show则是输出脉冲信号。

4.1.3 下载验证:

七彩灯环跑马效果如下:
image

4.2 Eg2_TM1640

本节我们目标是实现TM1640点亮16位共阴极数码管,显示“0~F”数字;

4.2.1硬件设计:

GPIO初始化配置 SCL(PC1) SDA(PC2);开漏输出,通过IO模拟I2C驱动TM1640;

4.2.1.1TM1640概述

TM1640 是一种LED(发光二极管显示器)驱动控制专用电路,内部集成有MCU 数字接口、数据锁存器、LED 驱动等电路。本产品性能优良,质量可靠。主要应用于电子产品LED显示屏驱动。采用SOP28、SSOP28的封装形式。

4.2.1.2特性说明
  • 采用CMOS工艺
  • 显示模式(8 段×16 位)
  • 辉度调节电路(占空比 8 级可调)
  • 两线串行接口(SCLK,DIN)
  • 振荡方式:内置RC振荡
  • 内置上电复位电路
  • 封装形式:SOP28、SSOP28
4.2.1.3管脚定义:

image

4.2.1.4管脚功能定义:

image

逻辑电源电压 为5V,SEG 脚驱动拉电流(VDD = 4.5~ 5.5 V、Vo = 0V)为90mA;GRID脚驱动灌电流200 mA;最大时钟频率为1MHz;

4.2.1.5接口说明

微处理器的数据通过两线总线接口和 TM1640 通信,在输入数据时当 CLK 是高电平时,DIN 上的信号必须保持不变;只有 CLK 上的时钟信号为低电平时,DIN 上的信号才能改变。数据的输入总是低位在前,高位在后传输.数据输入的开始条件是 CLK 为高电平时,DIN 由高变低;结束条件是 CLK 为高时,DIN 由低电平变为高电平。
指令数据传输过程如下图:
image

这里我们使用写 SRAM 数据地址自动加 1 模式:
image

Command1:设置数据
Command2:设置地址
data1~N:传输显示数据(最多 16 字节,不能多写)
Command3:控制显示

4.2.1.6数据指令

指令用来设置显示模式和LED 驱动器的状态。
在指令START有效后由DIN输入的第一个字节作为一条指令。经过译码,取最高B7、B6两位比特位以区别
不同的指令。
image

数据 命令设置:
image

地址命令设置:
image

上电时,地址默认设为C0H。
显示数据与芯片管脚以及显示地址之间的对应关系如下表所示:
image

显示控制 命令
image

4.2.2软件设计:

首先是TM1640初始化TM1640_GPIO_INIT,设置为开漏极输出,如下:

/*********************************************************************
 * @fn      TM1640_GPIO_INIT
 *
 * @brief   Initializes GPIOB.6 GPIOB.7
 *
 * @return  none
 */
void TM1640_GPIO_INIT(void) {

    GPIO_InitTypeDef GPIO_InitStructure = { 0 };

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_SetBits(GPIOB, GPIO_Pin_6);//SCL
    GPIO_SetBits(GPIOB, GPIO_Pin_7);//SDA
}

接着设置TM1640,并写入段码

void TM1640_Handle(void)
{
    u8 i;
    TM1640_Generate_START();
    TM1640_WriteData(0x40);//数据命令设置:普通模式,地址自动加一
    TM1640_Generate_STOP();

    TM1640_Generate_START();
    TM1640_WriteData(0xC0);////地址命令设置:初始地址00H
    for(i=0;i<16;i++)  //发送16位显示数据
    {
        TM1640_WriteData(CODE[i]);
    }
    TM1640_Generate_STOP();

    TM1640_Generate_START();
    TM1640_WriteData(0x8c);    //显示控制:显示开,脉冲宽度设为11/16
    TM1640_Generate_STOP();
    Delay_Ms(10);

}

4.2.3 下载验证:

显示"0~F";

image

4.3 Eg3_TM1650

本节我们目标是实现TM1650点亮4位共阴极数码管,并扫描27个轻触开关同时在数码管显示按键号。

4.2.1硬件设计:

GPIO初始化配置 SCL(PC1) SDA(PC2);开漏输出,通过IO模拟I2C驱动TM1650,TM1650IC驱动有点类似于I2C;

4.2.2软件设计:

首先是TM1650初始化TM1640_GPIO_INIT,设置为开漏极输出,并配置TM1650亮度,段数,工作模式,显示开关,并显示数码管为“0000”。

/*********************************************************************
 * @fn      TM1640_GPIO_INIT
 *
 * @brief   Initializes GPIOC.1 GPIOC.2
 *
 * @return  none
 */
void TM1650_GPIO_INIT(void) {

    GPIO_InitTypeDef GPIO_InitStructure = { 0 };
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_SetBits(GPIOC, GPIO_Pin_1); //SCL
    GPIO_SetBits(GPIOC, GPIO_Pin_2); //SDA
    Delay_Ms(500);

    TM1650_SystemCmd(0x00, 0x00, 0x00, 0x01);    //1级亮度+8段模式+正常工作模式+开显示
    DisplayNumber_4BitDig(0);
    printf("TM1650 Test Demo!\r\n");
}

接着我们通过TM1650_read读出键码,根据键码编号显示在数码上。

void TM1650ReadKey(void)
{
       unsigned char KeyDate=0x00;
      KeyDate = TM1650_read();
        switch(KeyDate)
        {
            /**单个按键**/
            case 0x44:TM1650Disp2Num(1);break;    //KI1+DIG1
            case 0x45:TM1650Disp2Num(2);break;      //KI1+DIG2
            case 0x46:TM1650Disp2Num(3);break;      //KI1+DIG3
            case 0x47:TM1650Disp2Num(4);break;      //KI1+DIG4

            case 0x4C:TM1650Disp2Num(5);break;      //KI2+DIG1
            case 0x4d:TM1650Disp2Num(6);break;      //KI2+DIG2
            case 0x4e:TM1650Disp2Num(7);break;      //KI2+DIG3
            case 0x4f:TM1650Disp2Num(8);break;      //KI2+DIG4

            case 0x54:TM1650Disp2Num(9);break;      //KI3+DIG1
            case 0x55:TM1650Disp2Num(10);break;     //KI3+DIG2
            case 0x56:TM1650Disp2Num(11);break;     //KI3+DIG3
            case 0x57:TM1650Disp2Num(12);break;     //KI3+DIG4

            case 0x5C:TM1650Disp2Num(13);break;     //KI4+DIG1
            case 0x5d:TM1650Disp2Num(14);break;     //KI4+DIG2
            case 0x5e:TM1650Disp2Num(15);break;     //KI4+DIG3
            case 0x5f:TM1650Disp2Num(16);break;     //KI4+DIG4

            case 0x64:TM1650Disp2Num(17);break;     //KI5+DIG1
            case 0x65:TM1650Disp2Num(18);break;     //KI5+DIG2
            case 0x66:TM1650Disp2Num(19);break;     //KI5+DIG3
            case 0x67:TM1650Disp2Num(20);break;     //KI5+DIG4

            case 0x6C:TM1650Disp2Num(21);break;     //KI6+DIG1
            case 0x6d:TM1650Disp2Num(22);break;     //KI6+DIG2
            case 0x6e:TM1650Disp2Num(23);break;     //KI6+DIG3
            case 0x6f:TM1650Disp2Num(24);break;     //KI6+DIG4

            case 0x74:TM1650Disp2Num(25);break;     //KI7+DIG1
            case 0x75:TM1650Disp2Num(26);break;     //KI7+DIG2
            case 0x76:TM1650Disp2Num(27);break;     //KI7+DIG3
            case 0x77:TM1650Disp2Num(28);break;     //KI7+DIG4

            /**组合按键**/
            case 0x7C:TM1650Disp2Num(29);break;     //KI1_KI2+DIG1
            case 0x7d:TM1650Disp2Num(30);break;     //KI1_KI2+DIG2
            case 0x7e:TM1650Disp2Num(31);break;     //KI1_KI2+DIG3
            case 0x7f:TM1650Disp2Num(32);break;     //KI1_KI2+DIG4

            default: break;
        }
}

4.2.3 下载验证:

显示"0~F";

image

posted @ 2022-10-15 10:08  LiJin_hh  阅读(2865)  评论(0编辑  收藏  举报