MM32F0140

1、I2C简介

I2C总线是一个两线串行接口,包含串行数据线(SDA)与串行时钟线(SCL),能够在连接到总线的器件间传递信息,每一个连接总线的设备都有独立的地址,主机可以通过该地址选择连接总线的设备并与之通信。I2C通过对SCL和SDA线高低电平时序的控制,来产生I2C总线协议所需要的信号,从而进行数据传输,微控制器可通过I2C总线接口实现芯片间串行互联。

I2C的功能框图如图1所示,当I2C采用主模式进行数据传输时,主机先发送从机设备地址与读写位数据,在从机地址匹配时可进行数据传输;I2C采用从模式时,从设备等待接收由主机发送来的地址数据,地址匹配时可进行数据传输。

“图1.I2C功能框图"
图1.I2C功能框图

主机与从机

主机是初始化总线的数据传输并产生允许传输的时钟信号的器件。此时,任何被寻址的器件都被认为是从机,在主模式下只支持一个主机。

设备地址

在进行数据传输前需要发送从设备地址,只有当主机的发送设备地址与从机的设备地址匹配时才能进行数据传输。总线的每个器件都有一个唯一的地址识别,而且都可以作为一个发送或接收器。

I2C信号

● 起始信号

当总线处于空闲状态时,SCL和SDA同时被外部上拉电阻拉为高电平。当主机启动数据传输时,必须先产生一个起始信号。在SCL线是高电平时, SDA线从高电平向低电平切换表示起始信号,如图2所示。在起始信号产生后,总线处于忙碌状态,除当前数据传输的主从设备外,其他I2C器件无法访问总线。

“图2.起始/停止信号"
图2.起始/停止信号

● 停止信号

停止信号如图2所示,当主机结束传输时要发送停止信号,在SCL线为高电平、SDA线由低电平向高电平切换时,产生停止信号,从而释放总线,总线再次处于空闲状态。

● 应答信号

I2C具有完善的应答机制,当主机发送完成地址与读写位或者主机发送一个字节的数据到从机上时,从机会回应一个应答信号(ACK),即在第9个SCL时钟周期时拉低SDA,如图3所示。

“图3.应答/非应答信号"
图3.应答/非应答信号

● 非应答信号

如图3所示,当第9个SCL时钟周期时拉高SDA,表示为非应答信号,主机或从机将产生一个停止信号中止传输。

图4.读写位

2、I2C配置

I2C的配置包括传输速率、从设备地址、数据传输方向及主从模式选择的配置,从而进行数据传输。

工作速率

I2C存在两种工作速率模式:标准模式(数据传输速率为0 ~ 100Kbps),快速模式(数据传输速率最大为400Kbps);可通过I2C控制寄存器(I2C_CR)的SPEED位进行控制,由于I2C从机速率跟随主机速率,因此只需在I2C为主模式下配置工作速率,如图5所示。

“图5.I2C工作速率"
图5.I2C工作速率

地址格式

I2C有两种地址格式:7位地址格式和10位地址格式。

● 7位地址

如图6所示,在起始条件(S)后发送的一个字节的前7位(A6 ~ A0)为从机地址,最低位(R/W)是数据方向位,当R/W位为0,表示主机写数据到从机,R/W位为1表示主机从从机读数据。

“图6.I2C
图6.I2C 7位地址

● 10位地址

在10位的地址格式中,发送2个字节来传输10位地址。发送的第一个字节的起始位后的5位(bit7:3)用于告示从机接下来是10位的传输,第一个字节的后两位(bit2:1)是从机地址的bit9:8,最低位(bit 0)是数据方向位(R/W)。传输的第二个字节为10位地址的低八位,如图7所示。

“图7.I2C
图7.I2C 10位地址

写操作

I2C进行写操作时,先通过I2C目标地址寄存器(I2C_TAR)配置从设备地址,再配置读写位,令控制数据命令寄存器(I2C_DR)的CMD位为0,所需传输数据放入I2C_DR寄存器的DR位,进行写操作。

读操作

I2C进行读操作时,先由主机写从设备地址及设备内寄存器地址,当从设备地址及设备内寄存器地址发送完成后,将控制数据命令寄存器(I2C_DR)的CMD位为1,进行读操作,读出设备内寄存器地址中的数值。

“图8.I2C
图8.I2C 读操作

主模式

I2C主模式配置需在I2C未使能状态下进行,将I2C使能寄存器(I2C_IMR)的ENABLE位置0。通过配置控制寄存器(I2C_CR)的SPEED位选择速率模式,MASTER10位选择地址格式,DISSLAVE位为1使从模式禁止,MASTER位为1选择主模式。向目标地址寄存器(I2C_TAR)写入从设备地址,I2C_IMR寄存器的ENABLE位置1,使能I2C。

I2C 接口支持读写的动态切换,当发送数据时,写数据到I2C的RX/TX数据缓冲及命令寄存器(DR)的低字节中,配置CMD位为0产生写操作。接下来的读命令,需要确保CMD位为1。如果发送FIFO为空,I2C模块拉低SCL直到下个命令写入到发送FIFO中。

从模式

I2C做从机时,等待其他主机的访问,只有其他主机发送了I2C从机对应的设备地址,才能进行数据传输。配置I2C为从模式,将I2C使能寄存器(I2C_IMR)的ENABLE位置0,禁止I2C使能,将I2C自身从地址写入从机地址寄存器(I2C_SAR),配置控制寄存器(I2C_CR)的SLAVE10选择寻址格式,DISSLAVE位为0,选择从模式,MASTER位为0,禁止主模式;I2C_IMR寄存器的ENABLE位置1,使能I2C。

当其他主机发送地址与 SAR 寄存器中的从机地址匹配,I2C接口响应发送的地址,并识别传输方向;当I2C产生读请求,从机将数据发送至主机;当主机为写操作时,I2C从机接收到主机的数据并将数据存储在接收缓冲中,软件通过读DR寄存器中的bit7:0来获得接收到的数据。

3、实验

本实验使用轮询进行数据的写操作与读操作,初始化配置I2C为主模式,从设备地址为0x50,速率为100KHz,使用7位地址格式。I2C初始化后,按下一次任意按键,进行8字节数据的写操作,其中第一字节写入数据设置为设备内的寄存器地址,串口显示写入数据;写操作完成后,再次按下任意按键,进行7位数据的读操作,从设备的寄存器地址中读出相关数据。

启用I2C外设时钟 enable_clock()

实验使用I2C1,串口打印输出结果,串口使用引脚属于GPIOA组,I2C使用引脚属于GPIOB组,因此需要启用I2C1、UART1、GPIOA及GPIOB的外设时钟。

void enable_clock()
{
    /* Enable I2C1 clock. */
    RCC->APB1ENR |= RCC_APB1_PERIPH_I2C1;
    /* Enable GPIOA clock. */
    RCC->AHB1ENR |= RCC_AHB1_PERIPH_GPIOA;
    /* Enable GPIOB clock. */
    RCC->AHB1ENR |= RCC_AHB1_PERIPH_GPIOB;
    /* Enable UART1 clock. */
    RCC->APB2ENR |= RCC_APB2_PERIPH_UART1;
}

配置引脚 pin_init()

配置I2C1引脚,I2C1_SCL为PB6引脚,I2C1_SDA为PB7引脚,复用通道为AF1;由于实验现象通过串口显示,故配置UART的TX(PA9)与RX(PA10)引脚。

void pin_init()
{
    /* PB6 - I2C1_SCL. */
    GPIOB->CRL &= ~GPIO_CRL_MODE6_MASK;
    GPIOB->CRL &= ~GPIO_CRL_CNF6_MASK;
    GPIOB->CRL |= GPIO_CRL_MODE6(GPIO_Speed_50MHz);
    GPIOB->CRL |= GPIO_CRL_CNF6(GPIO_PinMode_AF_OpenDrain);
    GPIOB->AFRL &= ~GPIO_AFRL_AFRY_MASK;
    GPIOB->AFRL |= (GPIO_AF_1 << GPIO_CRL_MODE6_SHIFT);

    /* PB7 - I2C1_SDA. */
    GPIOB->CRL &= ~GPIO_CRL_MODE7_MASK;
    GPIOB->CRL &= ~GPIO_CRL_CNF7_MASK;
    GPIOB->CRL |= GPIO_CRL_MODE7(GPIO_Speed_50MHz);
    GPIOB->CRL |= GPIO_CRL_CNF7(GPIO_PinMode_AF_OpenDrain);
    GPIOB->AFRL |= (GPIO_AF_1 << GPIO_CRL_MODE7_SHIFT);

    /* PA9 - UART_TX. */
    GPIOA->CRH &= ~GPIO_CRH_MODE9_MASK;
    GPIOA->CRH &= ~GPIO_CRH_CNF9_MASK;
    GPIOA->CRH |= GPIO_CRH_MODE9(GPIO_Speed_50MHz);
    GPIOA->CRH |= GPIO_CRH_CNF9(GPIO_PinMode_AF_PushPull);
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE9_SHIFT);

    /* PA10 - UART_RX. */
    GPIOA->CRH &= ~GPIO_CRH_MODE10_MASK;
    GPIOA->CRH &= ~GPIO_CRH_CNF10_MASK;
    GPIOA->CRH |= GPIO_CRH_CNF10(GPIO_PinMode_In_Floating);
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE10_SHIFT);
}

UART初始化 uart_init()

初始化UART,配置时钟频率、波特率、数据长度、停止位、传输模式及是否使用校验。

void uart_init()
{
    /* Clear the corresponding bit to be used. */
    UART1->CCR &= ~( UART_CCR_PEN_MASK | UART_CCR_PSEL_MASK | UART_CCR_SPB0_MASK | UART_CCR_SPB1_MASK | UART_CCR_CHAR_MASK );
    UART1->GCR &= ~( UART_GCR_AUTOFLOWEN_MASK | UART_GCR_RXEN_MASK | UART_GCR_TXEN_MASK );
    /* WordLength. */
    UART1->CCR |= UART_CCR_CHAR_MASK;
    /* XferMode. */
    UART1->GCR |= (UART_XferMode_RxTx << UART_GCR_RXEN_SHIFT);
    /* Setup baudrate, BOARD_DEBUG_UART_FREQ = 48000000u, BOARD_DEBUG_UART_BAUDRATE = 9600u. */
    UART1->BRR = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) / 16u;
    UART1->FRA = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) % 16u;
    /* Enable UART1. */
    UART1->GCR |= UART_GCR_UARTEN_MASK;
}

I2C初始化 i2c_init()

I2C通过操作配置寄存器(I2C_CR)实现初始化,配置I2C为主模式、速率为100KHz、7位地址格式、时钟频率为60000000u,设置从设备地址为0x50,使能I2C。

void i2c_init()
{
    I2C1->ENR &= ~I2C_ENR_ENABLE_MASK;
    uint32_t tmp = BOARD_I2C_FREQ / I2C_BaudRate_100K; /* BOARD_I2C_FREQ = 60000000u, I2C_BaudRate_100K = 100000u. */
    I2C1->SSHR = tmp / 2u - 12u; /* Configure high level count in normal speed. */
    I2C1->SSLR = tmp / 2u - 1u;  /* Configure low level count in normal speed. */
    I2C1->CR = I2C_CR_SPEED(1u);
    I2C1->CR &= ~I2C_CR_MASTER10_MASK;  /* Address format. */
    I2C1->CR |= I2C_CR_RESTART_MASK     /* Generate restart signal. */
             | I2C_CR_DISSLAVE_MASK     /* Disable slave module. */
             | I2C_CR_REPEN_MASK        /* Enable sending restart condition. */
             | I2C_CR_EMPINT_MASK       /* Control tx_empty interrupt generation. */
             | I2C_CR_MASTER_MASK;      /* Enable master module. */

    I2C1->RXTLR = 0u;  /* Configure the sending receive value. */
    I2C1->TXTLR = 0u;  /* Configure the sending threshold value. */

    /* Setup target address. */
    I2C1->TAR = I2C_TAR_ADDR(APP_I2C_TARGET_ADDR); /* APP_I2C_TARGET_ADDR = 0x50. */
    I2C1->ENR |= I2C_ENR_ENABLE_MASK;  /* Enable I2C. */
}

I2C写操作 i2c_write()

设置I2C发送数组i2c_tx_buf及发送数据长度i2c_tx_len,将数组i2c_tx_buf的第一个数值设置为寄存器地址,发送8字节数据。数据发送后读取状态寄存器(I2C_SR),等待发送缓冲空,再进行下一次发送;数据传输完成,操作使能寄存器的ABORT位为1,使I2C停止数据传输;读清除TX_ABRT中断寄存器(I2C_TXABRT),将TX FIFO从刷新/复位状态中释放。

void i2c_write()
{
    I2C1->DR = I2C_DR_DAT(i2c_tx_buf[0]);
    while (0u == (I2C1->SR & I2C_STATUS_TX_EMPTY) )
    {
    }
    for (uint32_t i = 1u; i < i2c_tx_len; i++)
    {
        I2C1->DR = I2C_DR_DAT(i2c_tx_buf[i]);

        while ( 0u == (I2C1->SR & I2C_STATUS_TX_EMPTY) )  /* Wait to tx fifo empty. */
        {
        }
    }
    I2C1->ENR |= I2C_ENR_ABORT_MASK;  /* Prepare for the stop. */
    I2C1->TXABRT;  /* Read register to release tx fifo. */
    while (I2C1->SR & I2C_STATUS_ACTIVE)  /* Wait to I2C not active, which means stop is taking effect. */
    {
    }
}

I2C读操作 i2c_read()

设置I2C接收数组i2c_rx_buf及接收长度i2c_rx_len,在进行数据读取前,需要发送从设备地址及寄存器地址,本实验设置发送数组的第一个数据为寄存器地址,发送完成后再将I2C_DR寄存器的读写位(CMD)置1,读取I2C_DR寄存器进行读操作。读操作结束后,操作使能寄存器的ABORT位为1,使I2C停止数据传输。

void i2c_read()
{
    I2C1->DR = I2C_DR_DAT(i2c_tx_buf[0]);
    while (0u == (I2C1->SR & I2C_STATUS_TX_EMPTY) )
    {
    }
    /* Read data from target device. */
    for (uint32_t i = 0u; i < i2c_rx_len; i++)
    {
        I2C1->DR = I2C_DR_CMD_MASK;  /* Swich read-write bit to read, prepare to get data. */

        while ( 0u == (I2C1->SR & I2C_STATUS_RX_NOTEMPTY) )  /* Wait to rx fifo not empty. */
        {
        }
        i2c_rx_buf[i] = (uint8_t)I2C1->DR;
    }
    I2C1->ENR |= I2C_ENR_ABORT_MASK;  /* Prepare for the stop. */
    I2C1->TXABRT;  /* Read register to release tx fifo. */
    while (I2C1->SR & I2C_STATUS_ACTIVE)  /* Wait to I2C not active, which means stop is taking effect. */
    {
    }
}

main()函数

main()函数结合上述操作,串口打印"i2c_master_basic example",初始化I2C后,将发送数组设置为0到7,按下一次任意按键,进行数据发送,串口显示发送数据;再次按下任意按键,进行数据读取,从寄存器地址中读出数值,设置寄存器地址设置为发送数据的首个数据,串口显示接收数据,实验结果如图9所示。

int main()
{
    enable_clock();
    pin_init();
    uart_init();
    printf("\r\ni2c_master_basic example\r\n");
    i2c_init();
    for (uint32_t i = 0u; i < i2c_tx_len; i++)
    {
        i2c_tx_buf[i] = i;
    }
    while (1)
    {
        printf("press any key to write i2c-eeprom.\r\n");
        getchar();
        i2c_write();
        printf("write data: ");
        for (uint32_t i = 0u; i < i2c_tx_len; i++)
        {
            printf("0x%02X, ", i2c_tx_buf[i]);
        }
        printf("\r\n");

        printf("press any key to read i2c-eeprom.\r\n");
        getchar();
        i2c_read();
        printf("read data from device register address: 0x%02X\r\n", i2c_tx_buf[0u]);
        printf("read data:  ");
        for (uint32_t i = 0u; i < i2c_rx_len; i++)
        {
            printf("0x%02X, ", i2c_rx_buf[i]);
        }
        printf("\r\n");
}

“图9.实验结果"
图9.实验结果

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 318

1、FlexCAN 简介

FlexCAN 控制器局域网模块是符合 ISO 11898-1 标准和 CAN 2.0B 规范的通信控制器,支持 CAN 总线协议。FlexCAN 模块框图如 图1 所示。FlexCAN 模块包括 CAN 收发器(CAN Transceiver)、协议引擎(Protocol Engine)、控制接口(Controller Host Interface)、总线接口单元(Bus Interface Unit),以及多个支持独立收发功能的信息缓冲区(Message Buffer)。其中 CAN 收发器负责将收发引脚的 TTL 信号转换为 CAN 总线的电平信号,协议引擎负责处理对信息缓冲区的读写请求,控制接口负责传输帧的发送仲裁以及接收匹配,总线接口单元处理 FlexCAN 和内部总线的交互,信息缓冲区用于存储 FlexCAN 的传输帧。

“图
图 1 MM32F0140 FlexCAN 模块框图

MM32F0140 的 FlexCAN 模块支持标准帧和扩展帧两种帧格式,支持数据帧和远程请求帧两种帧类型,其中数据帧的最大有效数据长度可达8比特。FlexCAN 模块支持最高可达 1Mps 的可编程比特率,支持对前 16 个信息缓冲区的中断,支持局部和全局的接收帧过滤机制,支持可选择的 FIFO 接收功能。

FlexCAN 模块支持三种工作模式:回环模式、只听模式和正常工作模式。因为在回环模式下,FlexCAN 的传输帧并不会通过其收发引脚发送到 CAN 总线,所以通常用于测试单块芯片的 FlexCAN 模块是否工作正常。只听模式下,FlexCAN 模块将只接收总线上的帧,而不能发送帧,并且也无法发送接收应答。正常工作模式下,FlexCAN 模块既可发送帧,也可接收帧。

2、FlexCAN 帧格式

FlexCAN 帧传输过程

“图
图 2 CAN 帧发送流程

图2 是 CAN 帧的发送流程,首先是帧起始位 SOF,SOF 值规定为 1,标识此帧的开始。在 CAN 总线协议中约定,值 0 为显性位,值 1 为隐性位。FlexCAN 采取多主机、基于优先级的总线传输方式,总线帧的收发顺序在 Arbitration field 仲裁段确定。仲裁成功的节点会继续发送帧,仲裁失败的节点会转为接收状态。如果不设置使用帧的优先级进行仲裁,则使用帧的 ID 号进行仲裁比较,越小优先级越高。结合 图2 CAN 帧发送流程 和 图3 CAN 标准数据帧格式可知,Arbitration field 仲裁段存放 CAN 报文的 ID 号以及 RTR 标识,RTR 用于表示帧类型,在下文讲述 FlexCAN 使用信息缓冲区发送报文时,会进一步解释其含义。Control filed 控制段在 MM32F0140 的 FlexCAN 中,主要涉及两个部分,IDE 字段和 DLE 字段。IDE 字段用于标识帧格式,为数据帧还是远程帧。DLC 字段用于标识此帧的有效字节数,DLC 字段值将影响后续的 Data field 字段长度。FDF 字段表示此帧是否为 CAN FD 帧,在MM32F0140 的 FlexCAN 模块中暂时未使用到。Data field 有效传输数据负载段,如果此帧为远程帧,将不携带任何数据,所以 DLC 字段对应为 0;如果此帧为数据帧,此段长度由 DLC 字段控制。CRC field 循环冗余码段,用于在发送和接收流程中检查此帧是否出现比特错误。CRC 后,为 ACK field 检测段,发送方会发送隐性位 1,然后在此段期间回读总线上信号。如果读到为显性位 0,则说明有其他 CAN 接收器接收到此帧,发送方发送成功。最后是 EOF 结束位,标识发送结束。

“图
图 3 CAN 标准数据帧格式

以下是 MM32F0140 FlexCAN 在上述 CAN 发送流程要求下,使用信息缓冲区寄存器进行 CAN 帧的收发的详细操作流程。信息缓冲区寄存器的结构如图 4 所示。

“图
图 4 FlexCAN 信息缓冲区结构图

FlexCAN 发送帧

FlexCAN 发送帧时,首先将帧相关信息写入预计使用的信息缓冲区寄存器中。如果此帧的 ID 号可用 11 个比特位进行表示,则将此帧的 ID 号填入 ID 段,并将 IDE 位置 0,标识为标准帧。如果此帧的 ID 号超过 11 位,则将剩余低位部分填入 ID(Extended) 段,并将 IDE 位置 1,标识为扩展帧。FlexCAN 的帧 ID 长度不能超过 29 位。

然后根据此帧的帧类型,如果是携带数据的数据帧,则将 SRR 替代远程请求位和 RTR 远程请求位都设置为 0,并将对应需要发送的数据写入 DataByte 区,将数据长度写入 DLC 字段;如果是不携带数据的远程请求帧,则将上述的 SRR 位和 RTR 位都设置为 1。

如果 MCR[LPRIOEN] 位被设置为 1,则在发送前还需要将此帧的优先级写入 PRIO 位,PRIO 位值越小,优先级越高。

当上述各字段都填充完毕后,将 CODE 值 12 填入 CODE 区,此 CODE 值表示将要发送此帧,发送完毕后 CODE 值变为 8,表示发送完毕。回读 TIME STAMP 段,可以得到发送成功时的时间戳。

FlexCAN 接收帧

FlexCAN 接收帧前,需要设置此接收信息缓冲区的 ID 号、帧格式 IDE 和帧类型 SRR 和 RTR,参考上述发送过程的设置。然后将 CODE 区值设置为值 4,表示等待接收。如果需要帧过滤,还需要设置局部帧过滤器 RXIMR 和全局帧过滤器 RXMGMASK、RX14MASK和RX15MASK。当 CAN 总线上有其他 FlexCAN 模块发送的帧时,信息缓冲区会将总线上的帧 ID 与自己的 ID 段以及掩码值进行匹配,接收帧,此时 TIME STAMP 段的值为接收时的时间戳。

在传输中会常使用到的 CODE 值如下表 1 所示。

表格 1 信息缓冲区 CODE 值含义

“表格

FlexCAN 功能测试流程

首先使用 FlexCAN 的回环模式测试开发板上的 FlexCAN 是否工作正常。回环测试成功以后可以进行 FlexCAN 正常工作模式的测试。如果使用的是两块开发板或使用 CAN 分析仪进行测试,需要注意总线两端必须连接 120Ω 终端电阻。在配置 FlexCAN 过程中,要保证收发方的波特率都配置成相同值。测试 FlexCAN 正常工作模式下收发的实验过程中,要确保至少有一个接收节点是正常工作模式,而非只听模式。

3、FlexCAN 配置

配置时钟

首先需要使能 FlexCAN 时钟,根据所使用的外设对 RCC 的 RCC_APB1ENR 寄存器进行赋值,将对应外设位置 1 即可使能时钟,详细外设如图 5 所示。

“图
图 5 MM32F0140 APB1总线外设

使能并初始化模块

使能 FlexCAN 模块,通过配置 FLEXCAN_MCR[MDIS] 为 0,可以使能 FlexCAN 模块。使能后,FlexCAN 模块会自动进入冻结模式。在冻结模式下,设置 FLEXCAN_MCR_SOFTRST 为 1,软件重置 FlexCAN 模块寄存器。然后将所有的信息缓冲区的独立掩码寄存器和全局掩码寄存器的掩码值都设置为 1,表示接收信息缓冲区将只接收 ID 号和缓冲区提前配置的 ID 号完全相同的报文,同时将每个信息缓冲区寄存器也全部清 0。

配置工作模式

如果 FlexCAN 需要进入正常工作模式,仅需在使能 FlexCAN、初始化信息缓冲区后退出冻结模式即可进入,即将 FLEXCAN_MCR[FRZ] 设置为 0。因为退出冻结模式需要一定时间,通过轮询 FLEXCAN_MCR[FRZACK] 是否等于 1 判断是否退出成功。如果 FlexCAN 需要配置为只读模式,则需要将 FLEXCAN_CTRL1[LOM] 置为 1;如果 FlexCAN 需要配置为回环模式,则需要将 FLEXCAN_CTRL1[LPB] 设置为 1。值得注意的是,当配置为回环模式时,FlexCAN 的 FLEXCAN_MCR[SRXDIS] 不能被设置为 1,否则 FlexCAN 将无法收到回环报文。

配置波特率

首先通过设置 FLEXCAN_CTRL1[CLKSRC] 指定 FlexCAN 协议引擎的时钟源,设置为 0 时使用振荡器时钟,设置为 1 时使用外设时钟。可详见图 6 FlexCAN 模块协议引擎时钟框图。

“图
图 6 FlexCAN模块协议引擎时钟框图

对 FlexCAN 引擎时钟频率进行可编程分频后得到 FlexCAN 串行时钟频率,FlexCAN 串行时钟频率的倒数被称为时间份额“图,用于衡量 FlexCAN 传输过程中各个阶段所需的时间长度,详见图 7 1 bit time 下 FlexCAN 传输段。

“图
图 7 1 bit time下FlexCAN传输段

SYNC_SEG 表示使用 1 个时间份额用于同步;Time Segment 1 段包括 PROPSEG 段和 PSEG1 段,前者用于补偿实际网络传输延时,后者和 PSEG2 都将用于补偿在 FlexCAN 传输过程中潜在的边缘相位误差。上述三项可以分别通过 FLEXCAN_CTRL1[PROPSEG]、FLEXCAN_CTRL1[PSEG1] 和 FLEXCAN_CTRL1[PSEG2] 进行设置。下图 8 是 Bosch CAN 2.0B 标准中对于 PSEG1 和PSEG2 的设置表。

“图
图 8 Bosch CAN 2.0B 标准中 PSEG1 和 PSEG2 的建议设置

在上述定义下,假定此时引擎时钟源的时钟频率为 ClockFreqHz,PROPSEG 中指定值为 PropSegLen,PSEG1 和PSEG2 中指定值分别为 PhaSegLen1 和 PhaSegLen2,所需波特率为 BaudRate 时,我们可以得到预期的分频数值 Div 为:

“MM32F0140学习笔记——FlexCAN

得到的 Div 值通过配置 FLEXCAN_CTRL1[PreDiv] 实现。这里尤其要注意各项和 ClockFreqHz 之间是否满足整除关系。

配置发送帧

首先需要将发送帧的内容填入等待发送的 i 号信息缓冲区的寄存器 FLEXCAN_MB[i] 的 CS、ID、WORD0 和WORD1 寄存器。然后将发送 CODE 值 (12) 填入 FLEXCAN_MB[i].CS[CODE] 中,即可发送。此时,如果将 FLEXCAN_ IMASK1 中对应中断位设置为 1,则发送成功后将会唤起中断。

配置接收帧格式

接收匹配需要设置对应的 i 号信息缓冲区 FLEXCAN_MB[i].CS[CODE] 值为 4,表示当前信息缓冲区接收为空,可以接收信息并安全存储,配置 FLEXCAN_MB[i].ID 为预期接收的帧 ID 号。设置 FLEXCAN_MB[i].CS[IDE] 配置接收扩展帧或标准帧,接收扩展帧则配置为 1,接收标准帧则配置为 0。设置 FLEXCAN_MB[i].CS[RTR] 配置接收远程帧或数据帧,接收远程帧则此位配置为 1,否则配置为 0。

如果需要对帧进行过滤接收,则需额外考虑配置全局帧过滤器或局部帧过滤器。配置 FLEXCAN_MCR[IRMQ] 为 1 则采用局部帧过滤,配置为 0 则采用全局帧过滤。当使用局部帧过滤方法时,掩码需要通过 FLEXCAN_RXIMRN[i] 寄存器配置,全局则通过 FLEXCAN_RXMGMASK、FLEXCAN_RX14MASK 和FLEXCAN_RX15MASK 寄存器进行配置。

4、FlexCAN 实验

在 SDK 中已有支持的 pokt-f0140 开发板上,在 driver example 下的 flexcan_loopback 样例中,使用 FlexCAN 模块进行回环测试,演示 FlexCAN 的初始化设置、接收和发送设置以及中断处理。

初始化外设时钟

FlexCAN 模块在 APB1 总线上,因此对 RCC_ APB1ENR 寄存器的 FLEXCAN 对应位设置为 1。
RCC_EnableAPB1Periphs(RCC_APB1_PERIPH_FLEXCAN, true);

初始化FlexCAN

使用外设时钟作为 FlexCAN 的时钟源,则 FlexCAN 的时钟频率为 pokt-f0140 上 APB1 总线时钟频率,为48MHz。此时将传输阶段的时钟配置 PhaseSegLen1,PhaseSegLen2,PropSegLen 分别配置为 2, 1, 1;将波特率APP_FLEXCAN_XFER_BAUDRATE 设置为 1MHz。则根据先前所述的波特率计算公式,可以满足整除关系。在实际调整波特率,需要考虑传输段的时钟设置是否满足整除关系。

/* Setup the flexcan module. */

void app_flexcan_init(void)
{
    /* Set the baudrate and bit time. */
    FLEXCAN_TimConf_Type flexcan_tim_conf;
    flexcan_tim_conf.EnableExtendedTime = false;/* No need to use extended time setting register. */
    flexcan_tim_conf.PhaSegLen1 = 2u;
    flexcan_tim_conf.PhaSegLen2 = 1u;
    flexcan_tim_conf.PropSegLen = 1u;

    /* Setup flexcan. */
    FLEXCAN_Init_Type flexcan_init;
    flexcan_init.MaxXferNum = APP_FLEXCAN_XFER_MaxNum; /* The max mb number to be used. */
    flexcan_init.ClockSource = FLEXCAN_ClockSource_Periph; /* Use peripheral clock. */
    flexcan_init.BaudRate = APP_FLEXCAN_XFER_BAUDRATE; /* Set baudrate. */
    flexcan_init.ClockFreqHz = CLOCK_APB1_FREQ; /* Set clock frequency. */
    flexcan_init.SelfWakeUp = FLEXCAN_SelfWakeUp_BypassFilter; /* Use unfiltered signal to wake up 
flexcan. */
    flexcan_init.WorkMode = FLEXCAN_WorkMode_LoopBack; /* Normal workmode, can receive and transport. */
    flexcan_init.Mask = FLEXCAN_Mask_Global; /* Use global mask for filtering. */
    flexcan_init.EnableSelfReception = true; /* Must receive mb frame sent by self. */
    flexcan_init.EnableTimerSync = true; /* Every tx or rx done, refresh the timer to start from zero. */
    flexcan_init.TimConf = &flexcan_tim_conf; /* Set timing sychronization. */
    /* Enable FlexCAN. */
    FLEXCAN_Init(BOARD_FLEXCAN_PORT, &flexcan_init);
}

配置接收信息缓冲区

配置接收 MB 的接收帧类型为标准数据帧,ID 为 APP_FLEXCAN_XFER_ID。通过配置接收 MB 的 CODE 区,将其配置为接收为空的状态。

/* Set rx mb. */
FLEXCAN_RxMbConf_Type flexcan_mb_conf;
flexcan_mb_conf.Id = APP_FLEXCAN_XFER_ID; /* Id for receiving. */
flexcan_mb_conf.MbType = FLEXCAN_MbType_Data; /* Only receive standard data frame. */
flexcan_mb_conf.MbFormat = FLEXCAN_MbFormat_Standard;
FLEXCAN_SetRxMb(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_RX_MB_CH, &flexcan_mb_conf);

/* Set for receiving frames. */
FLEXCAN_SetMbCode(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_RX_MB_CH, FLEXCAN_MbCode_RxEmpty);

配置发送信息缓冲区

将发送 MB 的寄存器清空。通过设置该 MB 的 CODE 区,将该 MB 设置为发送空闲。

/* Reset tx mb. */
FLEXCAN_ResetMb(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_TX_MB_CH);
FLEXCAN_SetMbCode(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_TX_MB_CH, FLEXCAN_MbCode_TxInactive);

配置接收中断

使能 FlexCAN 的接收中断,设置 NVIC。

/* Enable intterupts for rx mb. */
FLEXCAN_EnableMbInterrupts(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_RX_MB_INT, true);
NVIC_EnableIRQ(BOARD_FLEXCAN_IRQn);

中断处理函数

中断处理函数检查接收中断,设置接收全局标志位为真。

/* Interrupt request handler. */
void BOARD_FLEXCAN_IRQHandler(void)
{
    /* Check mb status. If received. */
    if (0u!= (FLEXCAN_GetMbStatus(BOARD_FLEXCAN_PORT) & BOARD_FLEXCAN_RX_MB_STATUS) )
    {
        /* Clear flexcan mb interrupt flag. */
        FLEXCAN_ClearMbStatus(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_RX_MB_STATUS);
        /* Update the flag. */
        app_flexcan_rx_flag = true;
    }
}

发送数据

设置标准数据帧的 ID 号,帧类型和帧格式以及优先级;然后填入数据;设置数据长度。将上述配置写入 MB 相关寄存器,然后修改 MB 的 CODE 区进行发送。

/* Send a message frame. */
void app_flexcan_tx(uint8_t * tx_buf)
{
    /* Prepare sending mb frame. */
    FLEXCAN_Mb_Type mb;
    mb.ID = APP_FLEXCAN_XFER_ID; /* Indicated ID number. */
    mb.TYPE = FLEXCAN_MbType_Data; /* Data frame type. */
    mb.FORMAT = FLEXCAN_MbFormat_Standard; /* Standard frame format. */
    mb.PRIORITY = APP_FLEXCAN_XFER_PRIORITY; /* The priority of the frame mb. */

    /* Set the information. */
    mb.BYTE0 = tx_buf[0];
    mb.BYTE1 = tx_buf[1];
    mb.BYTE2 = tx_buf[2];
    mb.BYTE3 = tx_buf[3];
    mb.BYTE4 = tx_buf[4];
    mb.BYTE5 = tx_buf[5];
    mb.BYTE6 = tx_buf[6];
    mb.BYTE7 = tx_buf[7];

    /* Set the workload size. */
    mb.LENGTH = APP_FLEXCAN_XFER_BUF_LEN;

    /* Send. */
    FLEXCAN_WriteTxMb(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_TX_MB_CH, &mb);
    FLEXCAN_SetMbCode(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_TX_MB_CH, FLEXCAN_MbCode_TxDataOrRemote); /* Write code to send. */
}

读取数据

读取对应的 MB 寄存器,将帧数据段内容解析出来。

/* Receive a message frame. */
void app_flexcan_read(uint8_t *rx_buf)
{
    /* Read the info from mb and reconstruct for understanding. */
    FLEXCAN_ReadRxMb(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_RX_MB_CH, &app_flexcan_rx_mb);
    rx_buf[0] = app_flexcan_rx_mb.BYTE0;
    rx_buf[1] = app_flexcan_rx_mb.BYTE1;
    rx_buf[2] = app_flexcan_rx_mb.BYTE2;
    rx_buf[3] = app_flexcan_rx_mb.BYTE3;
    rx_buf[4] = app_flexcan_rx_mb.BYTE4;
    rx_buf[5] = app_flexcan_rx_mb.BYTE5;
    rx_buf[6] = app_flexcan_rx_mb.BYTE6;
    rx_buf[7] = app_flexcan_rx_mb.BYTE7;
}

main() 函数

主函数会在键入之后,准备好数据并通过发送 MB 发送;等待中断处理函数将全局标志位设置为真后,解析接收MB 收到的帧,并打印。然后再等待下一次键入。

int main(void)
{
    BOARD_Init();
    printf("\r\nflexcan_loopback example.\r\n");

    /* Setup the flexcan module.*/
    app_flexcan_init();
    printf("press any key to send loop back frame with id %u.\r\n", (unsigned)APP_FLEXCAN_XFER_ID);

    while (1)
    {
        getchar();
        /* Send a message through flexcan. */
        for (uint8_t i = 0u; i < APP_FLEXCAN_XFER_BUF_LEN; i++)
        {
             app_flexcan_tx_buf[i] = ( app_flexcan_tx_buf[i] + i) % 256u;
        }
        app_flexcan_tx(app_flexcan_tx_buf);
        printf("app_flexcan_tx() done.\r\n");

        /* Wait for reception. */
        while (!app_flexcan_rx_flag) /* This flag will be on when the Rx interrupt is asserted. */
        {
        }
        app_flexcan_rx_flag = false;

        /* Read the message. */
        app_flexcan_read(app_flexcan_rx_buf); /* Send frame. */
        printf("app_flexcan_read(): ");
        for (uint8_t i = 0u; i < APP_FLEXCAN_XFER_BUF_LEN; i++)
        {
            printf("%u ", (unsigned)app_flexcan_rx_buf[i]);
        }
        printf("\r\n\r\n");
    }
}

实验结果

“图
图 9 实验结果

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 594

本篇笔记主要探讨 MM32F0140 定时器模块的框图结构、定时器提供的计数定时等功能以及配置定时器的流程,并以 pokt-f0140 开发板作为实际演示平台,使用开发板上 32 位定时器 TIM2 进行 pwm 波输出实验。

TIM 功能描述

MM32F0140 TIM 最基本的功能为计数定时,此功能依靠定时器内部的预分频器 PSC 、计数器 CNT 和 自动预装载寄存器 ARR 合作完成。此外,定时器如有输入输出通道,则还能提供输入捕获、比较输出和从模式输入等功能。在此之上,对于拥有额外刹车输入通道的高级定时器而言,还可提供刹车和死区设置。

TIM 框图

下图1 为 MM32F0140 高级定时器的结构框图,各部分支持不同的功能。其中:

● 红框支持最基本的计数定时功能。计数器、预分频器和自动预装载器也被称为定时器的时基单元。预分频器 PSC 将输入的时钟信号进行分频,计数器 CNT 对分频信号进行计数。计数器值和自动预装载寄存器 ARR 值进行比较,发生上溢或下溢时,则表明完成一次周期计数,周期值为 ARR 寄存器写入值,频率为输入时钟源经过 PSC 预分频器分频后频率。

● 灰框支持从模式输入功能。除内部时钟外,定时器模块可以通过配置从模式,选择来自外部输入引脚 TI1 或 TI2 的输入信号或 ETR 引脚信号或其他定时器的 ITRx 信号作为时钟输入源。

● 绿框支持输入捕获功能。当通道 x 配置为输入捕获模式时,通道 x 的输入信号依次经过滤波、边沿检测以及分频,触发通道 x 捕获事件。定时器模块会将当前计数器计数值写入对应通道的捕获/比较寄存器 CCRx 中。

● 蓝框支持比较输出功能。当通道 x 配置为比较输出模式时,定时器将当前计数器值与通道 x 捕获/比较寄存器 CCRx 中值进行比较,相等时定时器将改变通道 x 的参考输出电压 REF。

● 黑框支持刹车功能,当 BKIN 通道有指定刹车信号出现时,各路输出通道将输出预设的空闲输出电压。

“图1.
图1. MM32F0140 高级定时器框图

下文将详细叙述定时器的计数定时、输入捕获和比较输出这三个主要功能。

计数定时

计数定时即计数器使能后,在一定计数频率下进行计数。根据设置的计数方向,计数器完成周期次计数后,定时器触发更新中断。

计数器频率计算

设定时器输入频率为 clk_frq,预分频器分频值为 psc,则计数器频率 cnt_frq 为

“MM32F0140学习笔记——TIM"

计数器计数方向

MM32F0140 定时器中 TIM14、TIM16 和 TIM17 仅提供向上计数。其余定时器可以提供以下三种可选计数方向:

● 向上计数,计数器从 0 开始向上计数,递增到 ARR 自动预装载寄存器值后,计数器产生上溢事件。定时器产生一个更新事件,计数器又从 0 开始计数。

● 向下计数,计数器从 ARR 自动预装载寄存器值开始向下计数,递减到 0 后,计数器产生下溢事件。定时器产生一个更新事件,计数器又从 ARR 自动预装载寄存器值开始计数。

● 先向上计数再向下计数。计数器从 0 开始向上计数,递增到 ARR 自动预装载寄存器值后,计数器产生上溢事件。定时器产生一个更新事件。然后计数器从 ARR 自动预装载寄存器值开始向下计数,递减到 0 后,计数器产生下溢事件。定时器再产生一个更新事件。

“图2
图2 定时器向上计数

如图2 所示,定时器输入时钟即分频器时钟 CK_PSC,预分频器 PSC 值为 1 ,即定时器频率为 CK_PSC 的二分频频率。定时器自动装载值 ARR 为 5,从图中可见计数器 CNT 达到 5 时,产生一个更新事件 UEV。

定时器周期长度

ARR 自动预装载寄存器的值为定时器周期长度,决定定时器计数多少次后产生一个更新事件。

比较输出

如图3 所示,定时器比较输出功能通过计数器、通道 x 的捕获/比较寄存器 CCRx 以及输出控制电路实现。定时器将计数器和捕获/比较寄存器 CCRx 值实时比较,当二值相等时会改变通道 x 的参考输出电压 REF,参考输出电压 REF 和输出控制电路共同决定通道 x 的实际电压值。

“图3
图3 MM32F0140 定时器输出通道1示意图

比较输出匹配值

输出通道x 对应的 CCRx 寄存器值为比较输出匹配值,定时器将在计数器值和通道 x 的匹配值相等时,根据比较输出模式,改变通道 x 的参考输出电压REF。

比较输出模式选择

MM32 F0140 的 TIM 模块一共有 7 种比较输出模式:

● 匹配结果对于参考输出电压 REF 没有影响。

● 匹配时将参考输出电压 REF 设为高电平。

● 匹配时将参考输出电压 REF 设为低电平。

● 匹配时翻转参考输出电压 REF。

● 强制参考输出电压 REF为低电平。

● 强制参考输出电压 REF为高电平。

● PWM模式1:当计数值小于匹配值时,参考电压REF为高电平,否则为低电平。

● PWM模式2:当计数值小于匹配值时,参考电压REF为低电平,否则为高电平。

参考输出电压和实际输出电压间关系

定时器 CCER[CCxP] 位决定了参考输出电压和实际输出电压间关系,具体可见表1。CCER[CCxP]值为0表示高电平有效,为1表示低电平有效。

“表1
表1 CCER[CCxP]值对实际输出电压的影响

如下图 4 所示,通道1的比较值为1,当计数器值小于1时,通道1的比较输出电压 OC1REF 为高;当计数器值等于1时,通道1 的比较输出电压 OC1REF 拉低,产生一个下降沿。OC1REF 将一直保持为低,直到定时器更新事件发生,计数器从 0 开始计数时,OC1REF 又被拉高。

“图4
图4 递增计数下PWM模式1时输出通道参考电压

输入捕获

如图5 所示,输入通道 x 的输入信号 TIx 经过滤波器、边沿检测、分频以后被定时器捕获,定时器将输入捕获时刻的计数器值写入相应通道的捕获/比较寄存器 CCRx 中。

“图5
图5 MM32F0140输入捕获框图

计数定时配置

时基配置

配置定时器工作模式

配置 CR1[OPM] 位可以设置定时器工作模式,其中值为 0 表示定时器将循环计时,值为 1 表示计时 1 次后就停止计数器。

配置是否预装载

配置 CR1[ARPE] 位可以配置定时器自动预装载寄存器 ARR 的实际更新方式,值为1则用户写入 ARR 寄存器的值会在下一次计数器更新时起效,否则立刻生效。

配置计数模式

配置 CR1[DIR] 位和 CR1[CMS] 位可以配置计数器计数模式。如果选择单向计数,CR1[CMS]需配置为 0,此时CR1[DIR] 配置为 0 时,向上计数,配置为 1 时向下计数。如果选择先向上计数再向下计数,则需要配置 CR1[CMS],CR1[DIR] 值保持为 0。

配置预分频值

由上述计数器频率计算小结可知,如果需要定时器频率为 cnt_frq, 定时器输入源频率为 clk_frq,定时器的预分频值 psc 应为

“MM32F0140学习笔记——TIM"

计算出的 psc 值写入 PSC 寄存器。

配置自动预装载寄存器 ARR

自动预装载寄存器 ARR 的值决定定时器周期计数次数。假如定时器向上计数,则计数器从 0 递增到 ARR 值,即 ARR 寄存器值为 arrv,则实际计数器一个周期内会做arrv+1 次计数,所以ARR 寄存器值应为需要的周期计数值减一。将此值写入 ARR 寄存器。

使能计数器

完成上述配置后,将 CR1[CEN] 置为 1 即可启动计数器计数。当不需要计数时,将此位设置为 0。

比较输出配置

配置通道输出模式

配置 CCMRy[CCxS] 位为 0,可以将通道 x 配置为输出模式。

配置是否预装载匹配值

配置 CCMRy[OCxPE] 位可以设置通道 x 的比较输出匹配值生效方式,为 0 则一旦写入立刻生效,否则匹配值将在下一次更新事件后生效。

配置比较输出模式

配置 CCMRy[OCxM] 字段可以选择不同的比较输出模式,具体字段值和比较输出模式间关系可见表3。

“表3
表3 CCMRy[OCxM] 与 比较输出模式间对应关系

配置有效输出电压

配置 CCER[CCxP] 位,可以设置通道 x 的有效输出电压。值为0,则参考电压的高电平为有效电平;值为1,则参考电压的低电平为有效电平。

使能通道输出

配置 CCER[CCxE] 位为1,使能通道输出功能;配置为 0,则关闭通道输出功能。

输入捕获配置

配置通道输入模式

配置 CCMRy[CCxS] 位为1,可将通道 x 配置为输入模式。

配置输入信号边沿选择

配置 CCER[CCxP] 和 CCER[CCxNP] 位,可以配置输入信号的有效边沿选择,具体对应关系如表4。

“表4
表4 输入模式下,CCER[CCxP]和CCER[CCxNP]
对于输入边沿影响

配置采样和滤波

配置 CCMRy[ICxF] 字段,设置通道 x 的输入捕获滤波器。

配置预分频器

配置 CCMRy[IC1PSC] 字段,设置通道 x 输入信号的预分频值。

使能通道输入

配置 CCER[CCxE] 位为 1,使能通道输入捕获功能;配置为 0,则关闭通道输入捕获功能。

样例:pokt-f0140 开发板 定时器实现TIM2通道1输出pwm波

在 SDK 中已有支持的 pokt-f0140 开发板上,在 tim_32b_0 样例工程中,通过 tim_32b_output_compare_pwm 可以使用定时器 TIM2 的通道 1 输出 pwm 波。

时钟初始化

TIM2 在 APB2 总线上,需要使能时钟。TIM2 的通道 1 复用 PA0 引脚,需要使能 GPIOA 时钟。

/* Enable TIM. */
RCC_EnableAPB1Periphs(RCC_APB1_PERIPH_TIM2, true);
RCC_ResetAPB1Periphs(RCC_APB1_PERIPH_TIM2);
/* Enable GPIOA for TIM2_CH1. */
RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOA, true);
RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOA);

初始化输出引脚

GPIO_Init_Type gpio_init;
gpio_init.Pins  = GPIO_PIN_0;
gpio_init.PinMode  = GPIO_PinMode_AF_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_PinAFConf(GPIOA, gpio_init.Pins, GPIO_AF_2);/* 根据data sheet, 配置复用模式2。 */

定时器时基配置

TIM_32B_Init_Type tim_init;
tim_init.ClockFreqHz = BOARD_TIM_32B_FREQ;/* 因为 TIM_32B 在APB2总线上,所以BOARD_TIM_32B_FREQ的值实际为APB2总线时钟频率。*/
tim_init.StepFreqHz = APP_TIM_UPDATE_PERIOD;  /* 定时器周期时长为定时器周期长度Period加1后除定时器频率StepFreqHz。*/
tim_init.Period = APP_TIM_UPDATE_PERIOD - 1u; /* 所以可得值为1,也即定时器周期时长为1s。 */
tim_init.EnablePreloadPeriod = false; /* 不采用预装载,修改ARR寄存器将立即生效。 */
tim_init.PeriodMode = TIM_32B_PeriodMode_Continuous;/* 循环计时。 */
tim_init.CountMode = TIM_32B_CountMode_Increase; /* 递增计数。 */
TIM_32B_Init(BOARD_TIM_32B_PORT, &tim_init);

配置输出通道

TIM_32B_OutputCompareConf_Type tim_outcomp_conf;
tim_outcomp_conf.ChannelValue = 0u;/* Compare value initialize with 0. */
tim_outcomp_conf.EnableFastOutput = false; /* Disable fast output. */
tim_outcomp_conf.EnablePreLoadChannelValue = false; /* Disable preload, put data immediately. */
tim_outcomp_conf.RefOutMode = TIM_32B_OutputCompareRefOut_FallingEdgeOnMatch;/*Generate a falling edge when matched.*/
tim_outcomp_conf.ClearRefOutOnExtTrigger = false; /* Ext signal won't clear output. */
tim_outcomp_conf.PinPolarity = TIM_32B_PinPolarity_Rising;/* High polarity is valid. */
TIM_32B_EnableOutputCompare(BOARD_TIM_32B_PORT, BOARD_TIM_32B_CHANNEL, &tim_outcomp_conf);

使能计数器

TIM_32B_Start(BOARD_TIM_32B_PORT);

main 函数

main 函数将轮询键入,并按设定的占空比数组循环输出不同的 PWM 波。

int main(void)
{
    BOARD_Init();
    printf("\r\ntim_32b_output_compare_pwm.\r\n");

    /* Setup the timer. */
    app_tim_32b_init();

    printf("press any key to change the pwm ...\r\n");

    while (1)
    {
        for (uint32_t i = 0; i < APP_TIM_32B_PWM_NUM; i++)
        {
            getchar();
            TIM_32B_PutChannelValue(BOARD_TIM_32B_PORT, BOARD_TIM_32B_CHANNEL, app_tim_32_pwm_val[i]);/* Change duty cycle. */
            printf("PWM value: %u\r\n", (unsigned)app_tim_32_pwm_val[i]);
        }
    }
}

实验结果

将 TIM2 通道1 所在引脚用杜邦线与小灯泡引脚相连,可以实现呼吸灯的效果。因为每次键入,都会改变 pwm 波的占空比,所以小灯泡的亮度在不断改变:

“”

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 67

RCC 是 Reset and Clock Control 模块的缩写,可用于复位片上外设模块寄存器以及设置时钟树的时钟频率。本篇笔记将探讨 MM32F0140 微控制器上复位寄存器和设置时钟的操作流程,并以 pokt-f0140 开发板作为实际演示平台进行实验,实现 48MHz 开发板系统时钟。

RCC 功能描述

软件复位片上外设模块寄存器

通过设置 RCC_AHBRSTR 寄存器、RCC_APB1RSTR 寄存器以及 RCC_APB2RSTR 寄存器中对应外设模块位,可以实现软件复位片上外设模块寄存器。

设置时钟树频率

“图
图 1. RCC 时钟树框图

如图 1 所示,RCC 时钟树主要分成以下四部分:

  • 系统时钟
  • MCO时钟输出
  • 总线时钟
  • 外设时钟

下文将探讨 RCC 时钟树上系统时钟的配置流程、系统时钟与总线时钟的关系以及部分外设时钟与总线时钟的关系。

配置系统时钟

MM32F0140 支持四个独立时钟源:

  • 高速内部时钟(HSI)
  • 高速外部时钟 (HSE)
  • 锁相环(PLL)
  • 低速内部时钟(LSI)

通过配置 RCC_CFGR[SW] ,可以选择不同的时钟源作为系统时钟源。通过读取RCC_CFGR[SWS] 的值,可以确定系统时钟源选择。值得注意的是,当使能上述四个时钟源后,需要等待 RCC 寄存器中对应的时钟准备就绪标志被拉高,才能确认时钟源被使能完成。( eg. 配置 RCC_CR[HSION] = 1 也即使能高速内部时钟后, 需要等待 RCC_CR[HSIRDY] 被拉高后,才能确认 HSI 准备就绪。)

在上述四个独立时钟源中,PLL 配置相对复杂。PLL 频率取决于 PLL 输入时钟频率 FREFIN、 PLL 时钟倍频因子N 以及PLL 时钟分频因子M和P。PLL 频率 FCLKO 的计算公式为

FCLKO = FREFIN * N / ( M * P )

其中,输入时钟频率 FREFIN, 可以通过配置 RCC_PLLCFGR[PLLXTPRE] 和 RCC_PLLCFGR[PLLSRC] 进行选择 ,可选项有 HSI 输出时钟频率 、HSE 输出时钟频率以及 HSE 输出时钟的 2 分频频率。配置此项前,需要使能对应的时钟输入源。分频因子 M = RCC_PLLCFGR[PLL_DM] + 1, P = RCC_PLLCFGR[PLL_PM] + 1;倍频因子 N = RCC_PLLCFGR[PLL_DN] + 1。这三个系数可以通过配置 PLLCFGR 寄存器的相应位实现。M 值 和 P 值的区别在于,M 值是 PLL 对于输入时钟频率的分频, P 值是 PLL 作为系统时钟输出时的分频。

系统时钟与总线时钟

片上总线时钟由系统时钟一次或多次分频后得到。通过设置 RCC_CFGR[PPRE2] / RCC_CFGR[PPRE1] / RCC_CFGR[HPRE] 可以分别配置APB2,APB1 和 AHB1 总线时钟分频系数。总线时钟频率最高可达 72 MHz。

总线时钟与外设时钟

通过配置 RCC_AHBENR 寄存器、RCC_APB1ENR 寄存器以及 RCC_APB2ENR 寄存器的特定外设位,使能总线上外设时钟,从而可以允许总线对外设寄存器的读写操作。

对于大部分外设而言,外设时钟频率与外设所在总线频率相同。但有以下几个特例。

对于非 TIM1 的定时器而言,如果所在总线时钟分频系数不为 1,则定时器时钟频率为 2 倍频的总线时钟频率。TIM1 的时钟频率取决于 AHB 总线时钟和 APB2 总线时钟。如果 AHB 总线时钟和 APB2 总线时钟都不分频,则 TIM1 时钟频率和 APB2 时钟频率相同;如果 AHB 总线时钟和 APB2 总线时钟仅有一方分频,则 TIM1 时钟频率为 2 倍频的总线分频时钟频率。如果 AHB 总线时钟和 APB2 总线时钟都分频,则 TIM1 时钟频率为 4 倍频的 APB 总线时钟频率。

CPU 系统定时器频率 SysTick ,为 8 分频的 AHB 总线时钟频率。

样例

样例1 pokt-f0140 开发板初始化 48MHz 系统时钟

在 SDK 中已有支持的 pokt-f0140 开发板上,在任一样例工程中的 clock_init.c 中,通过 CLOCK_BootToHSE48MHz() 实现 48MHz 系统时钟和总线时钟。其中,HSE的输出时钟频率为12MHz。

主要代码如下:

void CLOCK_BootToHSE48MHz(void)
{
    /* enable HSE.使能HSE时钟。 */
    RCC->CR |= RCC_CR_HSEON_MASK;
    /* check HSE ready mask.时钟准备就绪。 */
    while ( RCC_CR_HSERDY_MASK != (RCC->CR & RCC_CR_HSERDY_MASK) )
    {
    }

    /* F_clko = F_refin * N/(M*P), F_refin = 12M. HSE输出频率为12MHz,倍频系数为8,分频系数为 1*2 = 2,PLL时钟频率为 12*8/2 = 48MHz。 */
    RCC->PLLCFGR = RCC_PLLCFGR_PLLSRC(1) /* (pllsrc == 1) ? HSE : HSI. */
                 | RCC_PLLCFGR_PLLDN(7) /* N = DN+1. */
                 | RCC_PLLCFGR_PLLDM(1) /* M = DM+1. */
                 | RCC_PLLCFGR_PLLDP(0) /* P = DP+1. */
                 | RCC_PLLCFGR_PLLLDS(1)
                 | RCC_PLLCFGR_PLLICTRL(3) /* 10uA. */
                 ;

    /* Enable PLL. 使能PLL时钟并等待时钟准备就绪。*/
    RCC->CR |= RCC_CR_PLLON_MASK;
    while((RCC->CR & RCC_CR_PLLRDY_MASK) == 0)
    {
    }

    /* Enable the FLASH prefetch. */
    RCC->AHB1ENR |= RCC_AHB1ENR_FLITFEN_MASK; /* enable the access to FLASH. */
    FLASH->ACR = FLASH_ACR_LATENCY(1u) /* setup divider: 1 for 48Mhz, 2 for 72MHz.. */
               | FLASH_ACR_PRFTBE_MASK /* enable flash prefetch. */
               ;

    /* Setup the dividers for each bus. 为各总线设置分频系数。 */
    RCC->CFGR = RCC_CFGR_HPRE(0)   /* div=1 for AHB freq. */
              | RCC_CFGR_PPRE1(0x0)  /* div=1 for APB1 freq. */
              | RCC_CFGR_PPRE2(0x0)  /* div=1 for APB2 freq. */
              | RCC_CFGR_MCO(7)    /* use PLL/2 as output. */
              ;

    /* Switch the system clock source to PLL. 选择系统时钟源为PLL。*/
    RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW_MASK) | RCC_CFGR_SW(2); /* use PLL as SYSCLK */


    /* Wait till PLL is used as system clock source.检查SWS标志,确认系统时钟源设置。 */
    while ( (RCC->CFGR & RCC_CFGR_SWS_MASK) != RCC_CFGR_SWS(2) )
    {
    }
}

样例2 pokt-f0140 开发板使能 GPIOA 模块时钟

在 SDK 中已有支持的 pokt-f0140 开发板上,可以通过以下代码使能 GPIOA 模块时钟。

RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOA, true);

样例3 pokt-f0140 开发板复位 GPIOA 模块寄存器

在 SDK 中已有支持的 pokt-f0140 开发板上,可以通过以下代码复位 GPIOA 模块寄存器。

RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOA);

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 51

COMP简介

COMP作为比较器,可用于比较模拟输入电压,并集成数字滤波器,其结果可输出至I/O口或定时器。

MM32F0140的COMP包含连接外部输入的COMP正相通道1 ~ 4,连接CRV及其他外部输入的COMP反相输入通道1 ~ 4;支持多种速率和功耗,支持比较结果的滤波功能,支持通过外部事件将CPU从睡眠和停机模式唤醒,带有轮询功能。

COMP的功能框图如图1所示,COMP存在多个正相输入与多个反相输入通道,正相输入可从各外部引脚之间选取,反相输入可从外部引脚或者CRV电压分压值,CRV的电压可选择VDDA或者内部参考电压(VREFINT)的分压。当正相输入电压高于反相输入电压,输出比较结果为高输出,当正相输入电压低于反相输入电压,输出比较结果为低输出,比较结果可输出至定时器或I/O口。

“图1.COMP功能框图"
图1.COMP功能框图

COMP配置

COMP的配置包含关于输出滤波、迟滞电压、输出极性、输出方向选择、正相输入选择、反相输入选择、功耗选择及比较器工作模式选择,其中在使用I/O端口用作比较器输入时,需配置对应引脚为模拟输入模式;比较器也具有锁定机制,可保证比较器的设置不会被无效寄存器访问或因程序计数器破坏而改变。

迟滞电压

迟滞电压能够防止噪声干扰,保证系统工作更加稳定,可通过操作比较器控制状态寄存器(COMPx_CSR)的HYST位,对迟滞电压进行配置。迟滞电压可配置为:90mV(HYST[1:0]=11)、30mV(HYST[1:0]=10)、15mV(HYST[1:0]=01)、0mV(HYST[1:0]=00)。

输出极性

输出极性用于设置输出为同相输出或反相输出,操作比较器控制状态寄存器(COMPx_CSR)的POL位,POL配置为1则为反相输出,POL位配置为0则为同相输出。

输出方向

通过操作比较器控制状态寄存器(COMPx_CSR)的OUT_SEL位,可配置输出结果到特定的定时器中,COMP输出方向列表如图2所示。

“图2.COMP输出方向"
图2.COMP输出方向

正相输入

操作比较器控制状态寄存器(COMPx_CSR)的INP_SEL位,可对比较器的正相输入信号源进行选择,共有4种信号源,正相通道如图3所示。

  • 正相通道0 COMPx_INP0(INP_SEL[1:0]=00)

  • 正相通道1 COMPx_INP1(INP_SEL[1:0]=01)

  • 正相通道2 COMPx_INP2(INP_SEL[1:0]=10)

  • 正相通道3 COMPx_INP3(INP_SEL[1:0]=11)

“图3.COMP正相输入"
图3.COMP正相输入

反相输入

操作比较器控制状态寄存器(COMPx_CSR)的INM_SEL位,可对比较器的反相输入信号源进行选择,部分反相通道如图4所示。

  • 反相通道0 COMPx_INM0(INM_SEL[2:0]=000)

  • 反相通道1 COMPx_INM1(INM_SEL[2:0]=001)

  • 反相通道2 COMPx_INM2(INM_SEL[2:0]=010)

  • 反相通道3 COMPx_INM3(INM_SEL[2:0]=011)

  • 反相通道4 COMPx_INM4(INM_SEL[2:0]=100)

“图4.COMP反相输入"
图4.COMP反相输入

功耗模式

在具体应用中,可通过调整比较器功耗和响应时间得到最优的结果,功耗模式共包含四种,能够通过软件操作比较器控制状态寄存器(COMPx_CSR)的MODE位进行功耗设置。

  • 高速/高功耗(MODE[1:0]=00)

  • 中速/中等功耗(MODE[1:0]=01)

  • 低速/低功耗(MODE[1:0]=10)

  • 极低速/极低功耗(MODE[1:0]=11)

输出滤波

操作比较器控制状态寄存器(COMPx_CSR)的OFLT位,可进行输出滤波的配置,若比较结果保持n个时钟周期不变,则输出有效,"n"为图5中所例举的时钟周期数。

“图5.输出滤波配置"
图5.输出滤波配置

工作模式

普通工作模式

COMP的输入通道可以在普通工作模式下通过软件选择,配置比较器控制状态寄存器(COMPx_CSR)的INP_SEL位和INM_SEL位选择正相输入与反相输入,将COMPx_CSR寄存器的EN位置1,使能比较器,COMP比较所选择的INP和INM端口上的信号,比较结果存放于COMPx_CSR寄存器的OUT位。若在配置COMP的INM_SEL位时选择CRV,则需要配置比较器外部参考电压寄存器(COMP_CRV)的CRV_SEL位,然后将CRV_EN置位。

轮询工作模式

COMP可以在轮询工作模式下通过硬件轮询的方式分时监测多个通道的比较结果,配置比较器轮询寄存器(COMPx_POLL)的PER1OD位来选择所需要的轮询等待周期,配置F1XN位决定INM端口的信号是否跟随INP端口轮询变化,配置POLL_CH位决定所需要轮询的通道是1/2/3 或者1/2,POLL_EN位置1启动轮询功能,再配置COMPx_CSR寄存器的EN位,使比较器开始上电工作,轮询比较的结果存放于COMPx_POLL寄存器的POUT位,其中POUT[2]、POUT[1]、POUT[0]位分别存放轮询通道3/2/1的比较结果。

锁定机制

为使比较器设置不会被无效寄存器访问或因程序计数器破坏而改变,可将比较器控制状态寄存器设置为写保护。操作比较器控制状态寄存器(COMPx_CSR)的LOCK位置1,使比较器锁定,整个COMPx_CSR寄存器变成只读,该位可由系统复位清零。

实验

本实验使用COMP的0号比较器,将正相输入通道0与反相输入通道0进行比较,通过串口打印当前的比较状态。配置比较器控制状态寄存器的OFLT位,设置为若比较结果保持4个时钟周期不变,则输出有效;配置HYST位使迟滞电压为90mV,配置POL位使输出极性为同相输出,配置OUT_SEL位为0使输出结果不进入任何TIM,配置INP_SEL与INM_SEL位选择正相输入通道0与反相输入通道0,配置MODE位使功耗为高速/高功耗;通过EN位置位使能COMP,给予正相输入通道与反相输入通道电压,通过串口观察比较结果。

启用COMP外设时钟 enable_clock()

实验使用COMP进行正相输入电压与反相输入电压对比,采用串口打印的方式观察实验结果,所使用的引脚均属于GPIOA组,因此需要启用COMP、UART1及GPIOA的外设时钟。

void enable_clock()
{
    /* Enable COMP clock. */
    RCC->APB2ENR |= RCC_APB2_PERIPH_COMP;
    /* Enable GPIOA clock. */
    RCC->AHB1ENR |= RCC_AHB1_PERIPH_GPIOA;
    /* Enable UART1 clock. */
    RCC->APB2ENR |= RCC_APB2_PERIPH_UART1;
}

配置引脚 pin_init()

配置COMP的正相输入通道0与反相输入通道0所对应的引脚,正相通道0的对应引脚为PA1,反相通道0的对应引脚为PA5,模式配置为模拟输入;由于实验现象通过串口显示,故配置UART的TX(PA9)与RX(PA10)引脚。

void pin_init()
{
    /* PA1 - COMP PInput_0. */
    GPIOA->CRL &= ~GPIO_CRL_MODE1_MASK;
    GPIOA->CRL |= GPIO_CRL_MODE1(GPIO_PinMode_In_Analog); /* PA1 Analog input. */

    /* PA5 - COMP NInput_0. */
    GPIOA->CRL &= ~GPIO_CRL_MODE5_MASK;
    GPIOA->CRL |= GPIO_CRL_MODE5(GPIO_PinMode_In_Analog); /* PA5 Analog input. */

    /* Setup PA9, PA10. */
    GPIOA->CRH &= ~GPIO_CRH_MODE9_MASK;
    GPIOA->CRH |= GPIO_CRH_MODE9(GPIO_PinMode_AF_PushPull);     /* PA9 multiplexed push-pull output. */
    GPIOA->AFRH &= ~GPIO_AFRH_AFR_MASK;
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE9_SHIFT);   /* Use AF1. */

    GPIOA->CRH &= ~GPIO_CRH_MODE10_MASK;
    GPIOA->CRH |= GPIO_CRH_MODE10(GPIO_PinMode_In_Floating);     /* PA10 floating input. */
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE10_SHIFT);    /* Use AF1. */
}

UART初始化 uart_init()

初始化UART,配置时钟频率、波特率、数据长度、停止位、传输模式及是否使用校验。

void uart_init()
{
    /* Clear the corresponding bit to be used. */
    UART1->CCR &= ~( UART_CCR_PEN_MASK | UART_CCR_PSEL_MASK | UART_CCR_SPB0_MASK | UART_CCR_SPB1_MASK | UART_CCR_CHAR_MASK );
    UART1->GCR &= ~( UART_GCR_AUTOFLOWEN_MASK | UART_GCR_RXEN_MASK | UART_GCR_TXEN_MASK );
    /* WordLength. */
    UART1->CCR |= UART_CCR_CHAR_MASK;
    /* XferMode. */
    UART1->GCR |= (UART_XferMode_RxTx << UART_GCR_RXEN_SHIFT);
    /* Setup baudrate, BOARD_DEBUG_UART_FREQ = 48000000u, BOARD_DEBUG_UART_BAUDRATE = 9600u. */
    UART1->BRR = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) / 16u;
    UART1->FRA = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) % 16u;
    /* Enable UART1. */
    UART1->GCR |= UART_GCR_UARTEN_MASK;
}

COMP初始化 comp_init()

实验使用COMP的0号比较器,操作比较器控制状态寄存器(COMP1_CSR),配置输出滤波、迟滞电压、输出极性、输出方向选择、正相输入通道、反相输入通道及功耗模式。

void comp_init()
{
    /* Output filter. */
    COMP->CSR[0] |= COMP_CSR_OFLT(COMP_OutFilter_4);  /* If the comparison result remains unchanged for four clock cycles, the output is valid. COMP_OutFilter_4 = 2. */
    /* Hysteresis. */
    COMP->CSR[0] |= COMP_CSR_HYST(COMP_Hysteresis_Alt3);  /* 90mV, COMP_Hysteresis_Alt3 = 3. */
    /* No invert output. */
    COMP->CSR[0] |= COMP_CSR_POL(false);  /* In-phase output. */
    /* Not output to other peripheral input. */
    COMP->CSR[0] |= COMP_CSR_OUTSEL(COMP_OutMux_None);  /* COMP_OutMux_None = 0. */
    /* Positive side. */
    COMP->CSR[0] |= COMP_CSR_INPSEL(COMP_InMux_Alt0);  /* COMP_INP[0], PA1, COMP_InMux_Alt0 = 0. */
    /* Inverse side. */
    COMP->CSR[0] |= COMP_CSR_INMSEL(COMP_InMux_Alt0);   /* COMP_INM[0], PA5, COMP_InMux_Alt0 = 0. */
    /* The faster the speed, the higher the power consumption. */
    COMP->CSR[0] |= COMP_CSR_MODE(COMP_Speed_High);  /* COMP_Speed_High = 0, high speed, high power. */
}

main()函数

main()函数结合上述操作,通过读取比较器控制状态寄存器(COMP_CSR)的OUT位,获取当前输出状态;本实验使对应引脚由杜邦线连接VCC或GND,从而给予正相输入通道与反相输入通道电压,每按下任意按键就获取一次当前输出状态,当正相输入高于反相输入,输出为高输出,串口打印"- positive";当正相输入低于反相输入,输出为低输出,串口打印"- inverse"。实验结果如图6所示,当PA1接VCC且PA5接GND时,当前输出状态为高输出,按下任意按键,串口显示"- positive";当PA1接GND且PA5接VCC时,当前输出状态为低输出,按下任意按键,串口显示"- inverse"。

void main()
{
    enable_clock();
    pin_init();
    uart_init();
    printf("comp_basic example.\r\n");
    comp_init();
    printf("press any key to get compare result ...\r\n");

    while (1)
    {
        getchar();
        if ( 0u != ( COMP_CSR_OUT_MASK & COMP->CSR[0] ) )
        {
            printf("- positive.\r\n"); /* The positive input voltage is higher than the negative input voltage. */
        }
        else
        {
            printf("- inverse.\r\n");  /* The positive input voltage is lower than the negative input voltage. */
        }
    }
}

图6.实验结果"
图6.实验结果

来源: 灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 156

SPI简介

SPI为串行外设接口,全称Serial Peripheral interface,是一种全双工、同步的通信总线,广泛用于不同设备之间的板级通讯。

MM32F0140的SPI支持接收和发送1 ~ 32位数据同时进行,主模式最大速率24Mbps,从模式最大速率12Mbps,支持一个主机与多个从机操作,支持DMA操作。

数据通信

在进行SPI数据通信时,通常由MOSI、MISO、SCK、NSS四个管脚与外部器件相连。如图1所示,MOSI管脚将来自主设备的数据输入到从设备,MISO管脚将从设备响应的数据传入主设备,从设备通过SCK管脚获得主设备提供的时钟信号,使发送和接收部分使用相同的时钟,保证数据传输的可靠性。NSS管脚进行从设备选择,使主设备可以和某个从设备一对一单独通信。

主设备数据输入(MISO)

MISO为主设备输入从设备输出管脚,传输方向为从设备发送到主设备。

主设备数据输出(MOSI)

MOSI为主设备输出从设备输入管脚,传输方向为主设备发送到从设备。

时钟(SCK)

SCK为串口时钟,控制数据交换的速率,由主设备产生,通过SCK引脚传输供从设备使用。

片选(NSS)

NSS为从设备选择管脚,SPI通过控制片选管脚NSS来控制多个从设备。当NSS引脚功能被激活后,配置作为主设备的SPI进入主模式,将会拉低NSS引脚,其余连接到主设备NSS的SPI设备由于检测到了NSS拉低的信号,会自动进入从设备模式。

“图1.SPI数据通信"
图1.SPI数据通信

数据传输时序

SPI可以通过配置时钟极性(CPOL)与相位(CPHA)选择四种不同的数据传输时序,即四种工作方式。

时钟极性(CPOL)

时钟极性是SCK在空闲时保持的电平状态,当CPOL配置为0时,SCK在空闲状态为低电平,即两次传输之间为低电平;当CPOL配置为1时,SCK在空闲状态为高电平,即两次传输之间为高电平。

时钟相位(CPHA)

时钟相位用于决定采样时刻,当CPHA配置为0时,第一个数据位采样从第一个时钟边沿开始;当CPHA配置为1时,第一个数据位采样从第二个时钟边沿开始;对数据进行边沿采样需要CPOL与CPHA的共同配置决定。

SCK低电平空闲,第一个沿采样

当CPOL=0且CPHA=0,第一位数据位在SCK时钟的第一个时钟上升沿被采样,如图2所示。

“图2.SCK低电平空闲且第一个沿采样"
图2.SCK低电平空闲且第一个沿采样

SCK高电平空闲,第一个沿采样

当CPOL=1且CPHA=0,第一位数据位的SCK时钟的第一个时钟下降沿被采样,如图3所示。

“图3.SCK高电平空闲且第一个沿采样"
图3.SCK高电平空闲且第一个沿采样

SCK低电平空闲,第二个沿采样

当CPOL=0且CPHA=1,第一个数据位在SCK时钟的第二个时钟下降沿被采样,如图4所示。

“图4.SCK低电平空闲且第二个沿采样"
图4.SCK低电平空闲且第二个沿采样

SCK高电平空闲,第二个沿采样

当CPOL=1且CPHA=1,第一个数据位在SCK时钟的第二个时钟上升沿被采样,如图5所示。

“图5.SCK高电平空闲且第二个沿采样"
图5.SCK高电平空闲且第二个沿采样

SPI配置

主模式

通过配置波特率发生器(SPI_I2S_SPBREG)设定串行时钟波特率,公式为:波特率=fpclk/SPBRG (fpclk是APB时钟频率)。配置通用控制寄存器(SPI_I2S_CCTL)的SPI数据宽度位(SPILEN),决定数据帧的长度是7位还是8位;配置时钟相位选择位(CPHA)与时钟极性标志位(CPOL),确定时序模式,为保证数据正常传输,主从设备的时序模式应保持配置一致;配置LSB在前使能位(LSBFE),决定数据位的输出顺序。操作全局控制寄存器(SPI_I2S_GCTL)的DW8_32位进行发送和接收数据寄存器有效数据选择,可配置为只有低8位有效或32位数据都有效;操作主机模式位(MODE)为1,选择主机模式;配置SPI/I2S选择位(SPIEN)为1,使能SPI。

若只接收而不发送数据,则配置接收数据个数寄存器(SPI_I2S_RXDNR),定义下次接收过程中需要接收字节的个数。

从模式

配置通用控制寄存器(SPI_I2S_CCTL)的LSB在前使能位(LSBFE),决定数据位的输出顺序是从最低有效位到最高有效位或从最高有效位到最低有效位;配置SPI数据宽度位(SPILEN),决定数据帧的长度是7位还是8位;配置时钟相位选择位(CPHA)与时钟极性标志位(CPOL),决定时序模式。配置全局控制寄存器(SPI_I2S_GCTL)的主机模式位(MODE)为0,选择从机模式;配置SPI/I2S选择位(SPIEN)为1,使能SPI。

数据发送

主模式

将需要发送的数据写入发送数据寄存器(SPI_I2S_TXREG),该寄存器的有效位由全局控制器(SPI_I2S_GCTL)的DW8_32位控制。在发送第一个数据位时,整个数据被传输到移位寄存器,后续数据通过移位寄存器串行输出到MOSI引脚。当中断状态寄存器(SPI_I2S_INTSTAT)的发送缓冲器有效中断标志位(TX_INTF)被置1,数据已从发送缓冲器被传输到移位寄存器。

从模式

当从设备收到SCK传来的时钟信号,同时接收到MOSI引脚传输的第一个数据位,从设备开始发送,第一个位被发送到MISO引脚,其余bit位被传输到移位寄存器,通过移位寄存器将数据串行发送。当中断状态寄存器(SPI_I2S_INTSTAT)的发送缓冲器有效中断标志位(TX_INTF)被置1,表示第一位已发送,其余位被传输到移位寄存器。

数据接收

主模式

从MISO引脚接收数据,数据通过移位寄存器,在最后一个采样时钟边沿后,数据字节被传输到接收缓冲器中。当中断状态寄存器(SPI_I2S_INTSTAT)的接收端数据有效中断标志位(RX_INTF)置1,数据接收完成,主模式下不再发送时钟信号。

从模式

从MOSI引脚接收数据,数据通过移位寄存器,在最后一个采样时钟边沿后,数据字节被传输到接收缓冲器中。当中断状态寄存器(SPI_I2S_INTSTAT)的接收端数据有效中断标志位(RX_INTF)置1,数据接收完成。

实验

本实验为回环测试,通过使用杜邦线连接SPI的MISO与MOSI引脚,实现数据的发送与接收。配置SPI主机,SPI进行一次数据发送与接收并对发送与接收信息进行验证,并通过串口打印传输情况,若有发送与接收数据不同的情况,串口打印出错信息与出错个数,若验证成功则打印"spi loopback xfer done."。

启用外设时钟 enable_clock()

实验使用SPI1,且需要通过串口打印实验现象,因此需启用SPI1与UART的外设时钟。

{
    /* Enable UART1 clock. */
    RCC->APB2ENR |= RCC_APB2_PERIPH_UART1;
    /* Enable GPIOA clock. */
    RCC->AHB1ENR |= RCC_AHB1_PERIPH_GPIOA;
    /* Enable SPI1 clock. */
    RCC->APB2ENR |= RCC_APB2_PERIPH_SPI1;
}

配置引脚 pin_init()

配置SPI的NSS(PA4)、MOSI(PA7)、MISO(PA6)、SCK(PA5)引脚,因为实验现象通过串口显示,所以配置UART的TX(PA9)与RX(PA10)引脚。

void pin_init()
{
    /* Setup NSS(PA4). */
    GPIOA->CHL &= ~GPIO_CRL_MODE4_MASK;
    GPIOA->CHL |= (GPIO_PinMode_AF_PushPull << GPIO_CRL_MODE4_SHIFT);  /* PA4 multiplexed push-pull output. */
    GPIOA->AFRL &= ~GPIO_AFRL_AFR_MASK;
    GPIOA->AFRL |= (GPIO_AF_0 << GPIO_CRL_MODE4_SHIFT);   /* Use AF0. */

    /* Setup MOSI(PA7). */
    GPIOA->CHL &= ~GPIO_CRL_MODE7_MASK;
    GPIOA->CHL |= (GPIO_PinMode_AF_PushPull << GPIO_CRL_MODE7_SHIFT);  /* PA7 multiplexed push-pull output. */
    GPIOA->AFRL |= (GPIO_AF_0 << GPIO_CRL_MODE7_SHIFT);   /* Use AF0. */

    /* Setup MISO(PA6). */
    GPIOA->CHL &= ~GPIO_CRL_MODE6_MASK;
    GPIOA->CHL |= (GPIO_PinMode_In_Floating << GPIO_CRL_MODE6_SHIFT);  /* PA6 floating input. */
    GPIOA->AFRL |= (GPIO_AF_0 << GPIO_CRL_MODE6_SHIFT);   /* Use AF0. */

    /* Setup SCK(PA5). */
    GPIOA->CHL &= ~GPIO_CRL_MODE5_MASK;
    GPIOA->CHL |= (GPIO_PinMode_AF_PushPull << GPIO_CRL_MODE5_SHIFT);  /* PA5 floating input. */
    GPIOA->AFRL |= (GPIO_AF_0 << GPIO_CRL_MODE5_SHIFT);   /* Use AF0. */

    /* Setup PA9, PA10. */
    GPIOA->CRH &= ~GPIO_CRH_MODE9_MASK;
    GPIOA->CRH |= (GPIO_PinMode_AF_PushPull << GPIO_CRH_MODE9_SHIFT);  /* PA9 multiplexed push-pull output. */
    GPIOA->AFRH &= ~GPIO_AFRH_AFR_MASK;
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE9_SHIFT);   /* Use AF1. */

    GPIOA->CRH &= ~GPIO_CRH_MODE10_MASK;
    GPIOA->CRH |= (GPIO_PinMode_In_Floating << GPIO_CRH_MODE10_SHIFT);  /* PA10 floating input. */
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE10_SHIFT);  /* Use AF1. */
}

UART初始化 uart_init()

初始化UART,配置时钟频率、波特率、数据长度、停止位、传输模式及是否使用校验。

void uart_init()
{
    /* Clear the corresponding bit to be used. */
    UART1->CCR &= ~( UART_CCR_PEN_MASK | UART_CCR_PSEL_MASK | UART_CCR_SPB0_MASK | UART_CCR_SPB1_MASK | UART_CCR_CHAR_MASK );
    UART1->GCR &= ~( UART_GCR_AUTOFLOWEN_MASK | UART_GCR_RXEN_MASK | UART_GCR_TXEN_MASK );
    /* WordLength. */
    UART1->CCR |= UART_CCR_CHAR_MASK;
    /* XferMode. */
    UART1->GCR |= (UART_XferMode_RxTx << UART_GCR_RXEN_SHIFT);
    /* Setup baudrate, BOARD_DEBUG_UART_FREQ = 48000000u, BOARD_DEBUG_UART_BAUDRATE = 9600u. */
    UART1->BRR = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) / 16u;
    UART1->FRA = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) % 16u;
    /* Enable UART1. */
    UART1->GCR |= UART_GCR_UARTEN_MASK;
}

SPI初始化 spi_init()

操作全局控制寄存器(SPI_I2S_GCTL)的MODE位,配置SPI为主模式,操作波特率发生器(SPI_I2S_SPBREG)配置波特率为400KHz,总线时钟频率为48MHz,操作通用控制寄存器(SPI_I2S_CCTL)的CPOL位与CPHA位配置通信模式,操作LSB在前使能位(LSBFE)令数据发送或接收最高位在前,操作全局控制寄存器对发送和接收数据寄存器有效数据进行选择(DW8_32),配置为只有低8位有效,设置NSS位置1,使硬件控制主模式下的NSS输出,配TXEN位与RXEN位置1,使能发送与接收;置SPI/I2S选择位(SPIEN)为1,使能SPI。

void spi_init()
{
    /* Master. */
    SPI1->GCTL = SPI_I2S_GCTL_MODE_MASK; /* Master mode. */
    /* XferMode. */
    SPI1->GCTL |= (SPI_I2S_GCTL_RXEN_MASK | SPI_I2S_GCTL_TXEN_MASK);  /* Enable TX and RX. */
    /* AutoCS. */
    SPI1->GCTL |= SPI_I2S_GCTL_NSS_MASK;  /* NSS select signal that from hardware. */
    /* BaudRate. */
    SPI1->SPBRG = 120u;  /* SPBRG = fpclk / baudrate = 48000000 / 400000 = 120. */
    SPI1->CCTL &= ~(SPI_I2S_CCTL_TXEDGE_MASK | SPI_I2S_CCTL_RXEDGE_MASK);  /* Sampling data in the middle of transmission data bits. */
    /* DataWidth. */
    SPI1->GCTL &= ~SPI_I2S_GCTL_DW832_MASK;  /* Only the lower 8 bits are valid. */
    /* CPOL & CPHA. */
    SPI1->CCTL &= ~(SPI_I2S_CCTL_CPHA_MASK | SPI_I2S_CCTL_CPOL_MASK);  /* CPOL = 0, CPHA = 0. */
    /* LSB first enable bit. */
    SPI1->CCTL &= ~SPI_I2S_CCTL_LSBFE_MASK;  /* The highest bit of data transmission or reception comes first. */
    /* Enbale SPI. */
    SPI1->GCTL |= SPI_I2S_GCTL_SPIEN_MASK;
}

SPI发送数据 spi_putbyte()

当发送缓冲器未满时,将数据传入发送数据寄存器(SPI_I2S_TXRFG),根据初始化配置,数据低8位有效,通过MOSI引脚串行输出。

void spi_putbyte(uint8_t c)
{
    while (SPI_I2S_CSTAT_TXFULL_MASK & SPI1->CSTAT)
    {}
    SPI1->TXREG = c;
}

SPI接收数据 spi_getbyte()

当接收端缓冲器接收了一个完整字节时,读接收数据寄存器(SPI_I2S_RXREG),返回接收数据。

uint8_t spi_getbyte() 
{
    while (0u == (SPI_I2S_CSTAT_RXAVL_MASK & SPI1->CSTAT) )
    {}
    return SPI1->RXREG;
}

main()函数

main()函数结合上述操作,初始化SPI,定义发送数组spi_tx_buf[16]并赋值,将spi_tx_buf[16]数组中的数值进行发送,定义接收数组spi_rx_buf[16]对数据进行接收。本实验使用杜邦线将MOSI引脚与MISO相连,因此,发送数据与接收数据应相同,将发送数组与接收数组的数值进行比较,定义变量spi_xfer_err_count用于计数发送与接收数值不同的数据个数。若发送与接收数值相同则串口输出"spi loopback xfer done.",若不同则串口输出传输错误与出错个数。实验现象如图6所示。

int main()
{
    enable_clock();
    pin_init();
    uart_init();
    printf("spi_basic example.\r\n");

    spi_init();

    for (uint32_t i = 0u; i < 16u; i++)
    {
        spi_tx_buf[i] = i;
    }

    /* SPI xfer once. */
    for (uint32_t i = 0u; i < 16u; i++)
    {
        spi_putbyte(spi_tx_buf[i]);
        spi_rx_buf[i] = spi_getbyte();
    }

    /* validation. */
    spi_xfer_err_count = 0u;
    for (uint32_t i = 0u; i < 16u; i++)
    {
        if (spi_rx_buf[i] != spi_tx_buf[i])
        {
            spi_xfer_err_count++;
        }
    }
    if (spi_xfer_err_count == 0u)
    {
        printf("spi loopback xfer done.\r\n");
    }
    else
    {
        printf("spi loopback xfer error. spi_xfer_err_count = %u\r\n", (unsigned)spi_xfer_err_count);
    }
    while (1)
    {}
}

“图6.实验现象"
图6.实验现象

来源: 灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 206

DMA简介

DMA全称为Direct Memory Access,即直接存储器访问。在进行DMA传输前,CPU将总线控制权交给DMA,通过共享系统总线,实现无需CPU参与的快速数据传输,能够直接将数据从一个地址空间复制到另一个地址空间,DMA在数据传输结束后将总线控制权交回CPU。

MM32F0140内置5路通用DMA,可以管理设备到存储器、存储器到设备与存储器到存储器的数据传输,每个通道都有专门的硬件DMA请求逻辑,也可以通过软件配置来触发每个通道。

MM32F0140的DMA模块所支持的外设类型包括UART、I2C、SPI、ADC与通用、高级和基础定时器,当DMA从外设产生的请求通过逻辑或输入DMA控制器时,为避免冲突,在一个通道中,同时只能有一个外设DMA请求有效,详细各通道DMA请求如图1所示。

“图1.各通道DMA外设请求"
图1.各通道DMA外设请求

DMA配置

DMA的配置涉及到传输模式、数据宽度、外设地址、存储器地址、通道优先级、数据传输数量、中断使能、自动重装载及指针增量。

传输模式

存储器到外设模式

配置DMA_CCRx寄存器(x由1 ~ 7)的DIR位选择传输方向,该位置1,传输方向为从存储器读;DMA_CCRx寄存器的MEM2MEM位置0,关闭存储器到存储器模式。

外设到存储器模式

配置DMA_CCRx寄存器的DIR位选择传输方向,该位置0,传输方向为从外设读;DMA_CCRx寄存器的MEM2MEM位置0,关闭存储器到存储器模式。

存储器到存储器模式

数据传输方向为外设地址到存储器地址:DMA_CCRx寄存器的MEM2MEM位置1,使能存储器到存储器模式;DIR位置0,从外设读。

数据传输方向为存储器地址到外设地址:DMA_CCRx寄存器的MEM2MEM位置1,使能存储器到存储器模式;DIR位置1,从存储器读。

注意,存储器到存储器模式不能与循环模式同时使用。

循环模式

若需要循环读写缓冲区或进行连续的数据传输,则可以进入循环模式。配置DMA通道x配置寄存器(DMA_CCRx)中的CIRC位为1,使能循环模式。在循环模式下,若DMA传输数量递减为0,则重新加载先前配置的数值,继续进行DMA数据传输。

自动重新加载

DMA通道x配置寄存器(DMA_CCRx)的ARE位控制自动重装载,若ARE位置1,则使能自动重装载传输数量,当DMA通道x传输数量寄存器(DMA_CNDTRx)中数值为0时,会自动将DMA_CNDTRx寄存器中的值加载为之前配置的数值;若ARE位置0,则禁止自动重装载传输数量。

数据宽度

DMA的数据宽度配置包含:存储器数据宽度配置与外设数据宽度配置,可独立配置为字节、半字、全字。

存储器数据宽度由DMA通道x配置寄存器(DMA_CCRx)的MSIZE位控制,MSIZE[1:0]为00则数据宽度为8bit,MSIZE[1:0]为01则数据宽度为16bit,MSIZE[1:0]为10则数据宽度为32bit。

外设数据宽度配置由DMA通道x配置寄存器(DMA_CCRx)的PSIZE位控制,可配置为8bit, 16bit或32bit,MSIZE位与PSIZE位如图2所示。

“图2.数据宽度对应位"
图2.数据宽度对应位

存储器/外设地址

DMA的地址配置包含:存储器地址的配置与外设地址的配置。

对DMA通道x存储器地址寄存器(DMA_CMARx)进行赋值,从而配置存储器地址,存储器地址可作为数据传输的源或目标。

对DMA通道x外设地址寄存器(DMA_CPARx)进行赋值,从而配置外设地址,外设地址是外设数据寄存器的基地址,作为数据传输的源或目标。

源和目标地址必须根据各自配置的数据传输宽度对齐。

指针增量

指针增量配置:每次传输后指针自动向后递增或保持不变。

操作DMA通道x配置寄存器(DMA_CCRx)的MINC位,若MINC位置1,则DMA配置为存储器地址递增模式,存储器的访问地址可以按照步长累加,不需要每次都去设置访问地址;若MINC位置0,则每次DMA传输固定访问同一个地址。

设置DMA通道x配置寄存器(DMA_CCRx)的PINC位,若PINC位置1,则DMA配置为外设地址递增模式,外设的访问地址可以按照步长累加;若PINC位置0,则每次DMA传输固定访问同一个地址。

外设或存储器配置为增量模式时,下一个要传输的地址将是前一个地址加上步长,步长取决于所选的数据宽度,首个传输的地址存放在DMA通道x外设地址寄存器(DMA_CPARx)/DMA通道x存储器地址寄存器(DMA_CMARx)中。

优先级

仲裁器根据通道请求的优先级来启动外设或存储器的访问,优先处理软件优先级高的请求,当软件优先级相同时,默认编号更低的通道优先处理。每个通道的优先级可在DMA通道x配置寄存器(DMA_CCRx)中的PL位配置,优先级分为四种:最高优先级(PL[1:0]=11)、高优先级(PL[1:0]=10)、中等优先级(PL[1:0]=01)与低优先级(PL[1:0]=00)。

数据传输数量

数据传输数量的最大值为65535,将数据传输数量值赋值到DMA通道x传输数量寄存器(DMA_CNDTRx),每次传输后,DMA_CNDTRx递减,表示剩余多少次DMA传输。

当数值递减为0时,数据全部传输完毕。若对应通道配置为自动重加载模式时,DMA_CNDTRx寄存器中的内容将被自动重新加载为之前配置时的数值。

中断

每个通道支持3种事件标志:DMA半传输(HTIF)、DMA传输完成(TCIF)和DMA传输出错(TEIF),各通道单独的中断请求由这3种事件标志逻辑或起来。可通过配置DMA通道x配置寄存器(DMA_CCRx)的TCIE位为1使能传输完成中断,配置HTIE位为1使能半传输中断,配置TEIE位为1使能传输错误中断。

实验

本实验演示DMA burst搬运模式的中断行为,配置DMA的传输模式为存储器到存储器模式且传输方向为从存储器读,开启自动重装载,宽度配置为全字,指针递增,优先级为低优先级,设置两数组为DMA数据传输地址,配置传输数量为16。DMA配置完成后,进行DMA数据传输,使能DMA通道,当产生半传输中断,设置半传输标志为true,当产生传输完成中断,设置传输完成标志为true,清除中断标志位。本实验可通过串口调试工具观察数据传输现象,当DMA传输已到一半时串口打印"half",当DMA传输完成时串口打印"done"。

启用外设时钟 enable_clock()

实验使用DMA1,并通过UART串口显示实验结果,因此需要启用DMA与UART的外设时钟。

void enable_clock()
{
    /* Enable UART1 clock. */
    RCC->APB2ENR |= RCC_APB2_PERIPH_UART1;
    /* Enable GPIOA clock. */
    RCC->AHB1ENR |= RCC_AHB1_PERIPH_GPIOA;
    /* Enable DMA1 clock. */
    RCC->AHB1ENR |= RCC_AHB1_PERIPH_DMA1;
}

配置引脚 pin_init()

实验现象通过串口显示,因此配置UART的TX(PA9)与RX(PA10)引脚。

void pin_init()
{
    /* Setup PA9, PA10. */
    GPIOA->CRH &= ~GPIO_CRH_MODE9_MASK;
    GPIOA->CRH |= GPIO_PinMode_AF_PushPull;     /* PA9 multiplexed push-pull output. */
    GPIOA->AFRH &= ~GPIO_AFRH_AFR_MASK;
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE9_SHIFT);   /* Use AF1. */

    GPIOA->CRH &= ~GPIO_CRH_MODE10_MASK;
    GPIOA->CRH |= GPIO_PinMode_In_Floating;     /* PA10 floating input. */
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE10_SHIFT);    /* Use AF1. */
}

UART初始化 uart_init()

初始化UART,配置时钟频率、波特率、数据长度、停止位、传输模式及是否使用校验。

void uart_init()
{
    /* Clear the corresponding bit to be used. */
    UART1->CCR &= ~( UART_CCR_PEN_MASK | UART_CCR_PSEL_MASK | UART_CCR_SPB0_MASK | UART_CCR_SPB1_MASK | UART_CCR_CHAR_MASK );
    UART1->GCR &= ~( UART_GCR_AUTOFLOWEN_MASK | UART_GCR_RXEN_MASK | UART_GCR_TXEN_MASK );
    /* WordLength. */
    UART1->CCR |= UART_CCR_CHAR_MASK;
    /* XferMode. */
    UART1->GCR |= (UART_XferMode_RxTx << UART_GCR_RXEN_SHIFT);
    /* Setup baudrate, BOARD_DEBUG_UART_FREQ = 48000000u, BOARD_DEBUG_UART_BAUDRATE = 9600u. */
    UART1->BRR = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) / 16u;
    UART1->FRA = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) % 16u;
    /* Enable UART1. */
    UART1->GCR |= UART_GCR_UARTEN_MASK;
}

DMA初始化 dma_init()

操作DMA_CCRx寄存器,配置输出传输模式、传输方向、外设和存储器的增量模式、外设和存储器的数据宽度、通道优先级和指针增量;操作DMA_CNDTRx寄存器,配置DMA传输数量,DMA传输完成一次,该数值减1,且在DMA传输期间DMA_CNDTRx寄存器不可被写入;操作DMA_CPARx寄存器,配置外设寄存器地址,由于本实验配置DMA传输方向为从存储器读,因此该外设地址在DMA传输时作为目标地址;操作DMA_CMARx寄存器,配置数据存储器的地址,DMA传输时从该存储器地址加载数据。

void dma_init()
{
    uint32_t ccr = 0u;
    ccr |= DMA_CCR_DIR_MASK;  /* Data transmission direction is: read from memory. */
         | DMA_CCR_MEM2MEM_MASK;  /* Xfer mode: memory to memory. */
         | DMA_CCR_ARE_MASK;   /* Enable automatic reloading. */
         | DMA_CCR_PINC(DMA_AddrIncMode_IncAfterXfer)  /* DMA_AddrIncMode_IncAfterXfer=1u, peripheral increment mode. */
         | DMA_CCR_MINC(DMA_AddrIncMode_IncAfterXfer)  /* Memory increment mode. */
         | DMA_CCR_PSIZE(DMA_XferWidth_32b)  /* DMA_XferWidth_32b = 2u, peripheral size 32 bits. */
         | DMA_CCR_MSIZE(DMA_XferWidth_32b)  /* Memory size 32 bits. */
         | DMA_CCR_PL(DMA_Priority_Low)  /* DMA_Priority_Low = 0u, low priority. */
         ;
    DMA1->CH[0].CCR = ccr;  /* channel number = 0 refer to DMA1_CH1. */
    DMA1->CH[0].CNDTR = DMA_BUFF_COUNT;  /* DMA_BUFF_COUNT = 16u, data transmission quantity. */
    DMA1->CH[0].CPAR = (uint32_t)app_dma_buff_to;  /* Set arry app_dma_buff_to as peripheral address. */
    DMA1->CH[0].CMAR = (uint32_t)app_dma_buff_from;  /* Set arry app_dma_buff_from as memory address. */
}

使能DMA enable_dma()

操作DMA_CCRx寄存器的ENABLE位,使能通道1,在通道使能后可进行DMA传输。

void enable_dma()
{
    DMA1->CH[0].CCR |= DMA_CCR_EN_MASK;  /* Enable DMA1 channel 1. */
}

使能中断 enable_interrupt()

操作DMA_CCRx寄存器的TCIE位置1,使能传输完成中断;HTIE位置1,使能半传输中断。

void enable_interrupt()
{
    DMA1->CH[0].CCR |= (DMA_CHN_INT_XFER_HALF_DONE & 0xEu);  /* DMA half transfer interrupt. */
    DMA1->CH[0].CCR |= (DMA_CHN_INT_XFER_DONE & 0xEu);  /* DMA end of transfer interrupt. */
}

配置NVIC NVIC_EnableIRQ()

使用Cortex-M0 core_cm0.h头文件中的NVIC_EnableIRQ使能中断,由于DMA初始化时配置使用通道1,因此使用DMA1通道1全局中断DMA1_CH1_IRQn.

NVIC_EnableIRQ(DMA1_CH1_IRQn)

编写中断服务程序DMA1_CH1_IRQHandler()

读DMA中断状态寄存器(DMA_ISR)获取当前中断状态,当产生半传输中断,令定义的半传输标志app_dma_xfer_half_done为true;当产生传输完成中断,令定义的传输完成标志app_dma_xfer_done为true。操作DMA中断标志清除寄存器(DMA_IFCR)对应位置1,清除对应中断。

void DMA1_CH1_IRQHandler()
{
    uint32_t flags = DMA1->ISR & 0xFu;

    if (flags & DMA_CHN_INT_XFER_HALF_DONE)  /* DMA half transfer interrupt. */
    {
        app_dma_xfer_half_done = true;
    }

    if (flags & DMA_CHN_INT_XFER_DONE)  /* DMA end of transfer interrupt. */
    {
        app_dma_xfer_done = true;
    }

    DMA1->IFCR = (flag & 0xFu);  /* Clear interrupt. */
}

main()函数

main()函数结合上述操作,初始化DMA,设置变量app_dma_xfer_done为传输完成标志,设置变量app_dma_xfer_half_done为DMA已传输一半的标志,设置数组app_dma_buff_from[]为源,即DMA传输时从该存储器地址加载数据,设置数组app_dma_buff_to[]为数据存储地址,设置变量DMA_BUFF_COUNT为数据传输数量,传输数量为16,每次传输后,该数值递减,当数值递减为0时,数据传输完毕,配置自动重加载模式,数据传输数量会被自动重新加载为之前配置时的数值。当检测到半传输中断标志产生,串口打印"half",检测到传输完成中断标志产生,串口打印"done"。本实验在串口输入数据时进行DMA传输,将源地址app_dma_buff_from[]中的数据通过DMA传输到app_dma_buff_to[]地址中。实验现象如图3所示。

int main()
{
    uint8_t c;

    enable_clock();
    pin_init();
    uart_init();

    printf("\r\ndma_burst_interrupt example.\r\n");

    dma_init();
    NVIC_EnableIRQ(DMA1_CH1_IRQn);
    enable_interrupt();

    for (uint32_t i = 0u; i < DMA_BUFF_COUNT; i++)
    {
        app_dma_buff_from[i] = i;
        app_dma_buff_to[i] = 0u;
    }

    while (1)
    {
        c = getchar();
        putchar(c);

        for (uint32_t i = 0u; i < DMA_BUFF_COUNT; i++)
        {
            app_dma_buff_to[i] = 0u;
        }
        app_dma_xfer_done = false;
        app_dma_xfer_half_done = false;

        enable_dma();

        while (!app_dma_xfer_half_done)
        {}
        printf("half.\r\n");
        app_dma_xfer_half_done = false;        

        while (!app_dma_xfer_done)
        {}
        printf("done.\r\n");
        app_dma_xfer_done = false;
    }
}

“图3.实验现象"
图3.实验现象

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 129

UART简介

UART是通用异步收发器,全称为Universal Asynchronous Receiver and Transmitter,属于异步串口通信协议的一种,能够灵活进行全双工数据交换。

MM32F0140的UART支持全双工数据交换、同步单向通信、半双工单线通信、多处理器之间的通信以及调制解调器(CTS/RTS)操作。

串行通信

串行通信是指使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度,只需要少数几条线就可以在系统间交换信息。

串行通信按照数据传输方向分为:

● 单工通信

数据只能在一个方向上传输,通常采用两线进行通信,分别是:GND、TX(发送数据输出引脚)或RX(接收数据输入引脚),发送设备与接收设备共地将参考电压调节一致,MCU做发送或接收。

● 半双工通信

相当于可切换方向的单工通信,在具体时刻,只允许数据在一个方向上传输,不能同时在两个方向上传输。

● 全双工通信

允许数据同时在两个方向上传输,通常采用三线,分别是:GND、TX、RX,接收设备与发送设备均为双向通信设备,若通信双方有一方需为另一方提供电源,则两设备的VDD相连。

异步通信

异步通信过程中,接收器和发送器使用各自的时钟,以一个字符为传输单位,通信中两个字符间的时间间隔不固定,但在同一个字符中的两个相邻位间的时间间隔固定,每一个字符要用起始位和停止位作为字符开始和结束的标志。

UART功能

如图1所示,Device1做发送器,Device2做接收器进行通信,发送器对发送数据执行“并->串”转换,然后,数据从发送器的发送数据输出引脚(TX)输出,在传输线路上一位一位的传输到接收器的接收数据输入引脚(RX),接收器对接收到的数据进行“串->并”转换。

“图1.
图1. UART通信

数据传输

UART的数据传输如图2所示,包含起始位、数据帧、奇偶校验位、停止位、空闲帧与断开帧。

● 起始位

在发送器被使能,且无数据发送时,TX引脚处于高电平,若要进行数据传输,发送器会在发送起始位拉低TX引脚,即将传输线从高电平拉到低电平并保持1个时钟周期。

● 数据帧

数据帧包含需要传输的数据,数据长度由UART通用控制寄存器(UART_CCR)的CHAR位配置,通常可以设置为5 ~ 8位,若不使用奇偶校验位,数据帧长度可为9位。

发送数据需要将UART全局控制寄存器(UART_GCR)的TXEN位置1,数据从UART发送数据寄存器(UART_TDR)写入,经过一字节缓冲器缓冲,最后通过发送移位寄存器,以最低字节到最高字节的顺序,串行在TX引脚上输出。

接收数据需要将UART全局控制寄存器(UART_GCR)的RXEN位置1,读UART接收数据寄存器(UART_RDR)可获取接收到的数据并清零中断状态寄存器(UART_ISR)的RX_INTF(接收有效数据中断标志)。

● 奇偶校验位

检验数据中1的总个数为奇或偶,判断传输器件数据是否发生改变。奇偶校验可以通过UART通用控制寄存器(UART_CCR)的PEN位置1使能发送接收校验,UART_CCR寄存器的PSEL位为1则数据偶校验,PSEL位为0则数据奇校验。

奇校验:若数据位中1的数目是偶数,则校验位为1,如果1的数目是奇数,校验位为0。

偶校验:若数据位中1的数目是偶数,则校验位为0,如果1的数目是奇数,校验位为1。

● 停止位

停止位用1表示一帧的结束,可通过配置UART通用控制寄存器(UART_CCR)的SPB0位设置停止位位数,位数可设置为0.5、1、1.5、2个停止位。

● 空闲帧

包括停止位在内,一个完全由1组成的完整数据帧,定义为一个空闲符号,下一个数据帧的起始位跟在空闲符之后。

● 断开帧

包括停止位在内,一个完全由0组成的完整数据帧,定义为一个断开符号,在断开帧结束时,发送器再发送一个停止位1,使得下一帧的起始位能够被识别到(产生下降沿被检测到)。断开符号通过设置UART_CCR寄存器的BRK位进行发送,若BKP位置1,在当前数据发送完成后,将会发送一个断开符号到TX引脚上。

“图2.
图2. UART数据传输

波特率

波特率表示数据传输速率,波特率发生器产生时钟,经过发送器和接收器的使能位置位控制后,供给发送或接收使用。对于大多数串行通信,需要将发送和接收设备的波特率设置为相同的值,若波特率不同,则发送与接收数据的时序可能受到影响。波特率的计算公式如图3所示,UART波特率寄存器(UART_BRR)存放UART分配器除法因子(UARTDIV)的整数部分,UART分数波特率寄存器(UART_FRA)存放UARTDIV的小数部分。例如,若系统时钟为48M,配置波特率为9600(每秒传输9600bit的数据),则(48000000 /9600) / 16的结果赋值到UART_BRR寄存器中,(48000000 / 9600) % 16的取余结果赋值到UART_FRA寄存器中。

“图3.
图3. UART的波特率公式

实验

本实验配置UART的基本发送与接收功能,配置时钟速率为48MHz,波特率为9600,数据长度为8位,不使用校验及自动流控制,设置PA9为TX引脚,PA10为RX引脚。通过串口调试工具观察数据的传输,发送数据与接收数据相同。

配置系统时钟 clock_init()

如图4所示,高速外部时钟(HSE)的频率范围为4 ~ 24MHz,实验所使用的晶振为12M,要使系统时钟为48MHz,则配置PLL输出48MHz做系统时钟,操作时钟控制寄存器(RCC_CR)的HSEON位使能HSE,等待HSERDY位拉高(即HSE时钟被释放),设置PLL配置寄存器(RCC_PLLCFGR)中的PLLSRC位为1,并根据公式配置对应参数,PLL配置公式如图5所示。配置闪存访问控制寄存器(FLASH_ACR)启用闪存预取,配置时钟配置寄存器(RCC_CFGR)设置分频并配置PLL输出做系统时钟。

void clock_init()
{
    /* Enable HSE. */
    RCC->CR |= RCC_CR_HSEON_MASK;
    while ( RCC_CR_HSERDY_MASK != (RCC->CR & RCC_CR_HSERDY_MASK ) ) /* Waiting HSE ready. */
    {
    }
    /* F_clko = F_refin * N/(M * P), F_refin = 12M, 12*8/(1*2) = 48. */
    RCC->PLLCFGR = RCC_PLLCFGR_PLLSRC(1)  /* HSE clock is used as PLL input clock. */
                 | RCC_PLLCFGR_PLLDN(7)   /* N = DN + 1 = 7 + 1 = 8. */
                 | RCC_PLLCFGR_PLLDM(1)   /* M = DM + 1 = 1 + 1 = 2. */
                 | RCC_PLLCFGR_PLLDP(0)   /* P = DP + 1 = 0 + 1 = 1. */
                 | RCC_PLLCFGR_PLLLDS(1)  /* PLL lock detector accuracy select. */
                 | RCC_PLLCFGR_PLLICTRL(3)  /* 10uA. */
                 ;
    /* Enable PLL. */
    RCC->CR |= RCC_CR_PLLON_MASK;
    while( 0u == ( RCC->CR & RCC_CR_PLLRDY_MASK ) ) /* Waiting PLL ready. */
    {
    }
    /* Enable the FLASH prefetch. */
    RCC->AHB1ENR |= RCC_AHB1ENR_FLITFEN_MASK; /* Enable the access to FLASH. */
    FLASH->ACR = FLASH_ACR_LATENCY(1u)        /* Setup divider: 1 for 48Mhz. */
               | FLASH_ACR_PRFTBE_MASK        /* Enable flash prefetch. */
               ;
    /* Setup the dividers for each bus. */
    RCC->CFGR = RCC_CFGR_HPRE(0)     /* Div=1 for AHB freq. */
              | RCC_CFGR_PPRE1(0x0)  /* Div=1 for APB1 freq. */
              | RCC_CFGR_PPRE2(0x0)  /* Div=1 for APB2 freq. */
              | RCC_CFGR_MCO(7)      /* Use PLL/2 as output. */
              ;
    /* Switch the system clock source to PLL. */
    RCC->CFGR = ( (RCC->CFGR & ~RCC_CFGR_SW_MASK ) | RCC_CFGR_SW(2) ); /* Use PLL as SYSCLK. */
    /* Wait till PLL is used as system clock source. */
    while ( (RCC->CFGR &  RCC_CFGR_SWS_MASK ) != RCC_CFGR_SWS(2) )
    {
    }
}

“图4.
图4. MM32F0140部分时钟树

“图5.
图5. PLL配置公式

启用UART外设时钟 enable_clock()

UART1的UART1_TX与UART1_RX复用引脚为PA9与PA10,因此初始化GPIOA与UART1的外设时钟,UART1在APB2上,GPIOA在AHB上。

void enable_clock()
{
    /* Enable UART1 clock. */
    RCC->APB2ENR |= RCC_APB2_PERIPH_UART1;
    /* Enable GPIOA clock. */
    RCC->AHB1ENR |= RCC_AHB1_PERIPH_GPIOA;
}

配置引脚 pin_init()

由于UART的TX与RX引脚配置为复用功能配置,如图6所示,PA9, PA10的UART1_TX与UART1_RX均使用AF1,对端口复用功能高位寄存器(GPIO_AFRH)的端口9、端口10对应位赋值。

void pin_init()
{
    /* Setup PA9, PA10. */
    GPIOA->CRH &= ~GPIO_CRH_MODE9_MASK;
    GPIOA->CRH |= GPIO_PinMode_AF_PushPull;     /* PA9 multiplexed push-pull output. */
    GPIOA->AFRH &= ~GPIO_AFRH_AFR_MASK;
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE9_SHIFT);   /* Use AF1. */

    GPIOA->CRH &= ~GPIO_CRH_MODE10_MASK;
    GPIOA->CRH |= GPIO_PinMode_In_Floating;     /* PA10 floating input. */
    GPIOA->AFRH |= (GPIO_AF_1 << GPIO_CRH_MODE10_SHIFT);    /* Use AF1. */
}

“图6.
图6. 部分引脚复用表格

UART初始化 uart_init()

初始化UART需要配置:时钟频率、波特率、数据长度、停止位、传输模式及是否使用校验。

如图7所示,UART全局控制寄存器(UART_GCR)的TXEN位与RXEN位控制传输模式,两位均置1表示传输模式为TX与RX,AUTOFLOWEN位控制是否使用自动流控制,UARTEN位控制UART的使能。

如图8所示,UART通用控制寄存器(UART_CCR)的SPB1、SPB0位控制停止位位数,CHAR位控制数据宽度,PSEL位选择采用奇校验还是偶校验,PEN位控制校验使能;UART波特率寄存器(UART_BRR)与UART分数波特率寄存器(UART_FRA)分别存储UART分频器除法因子的整数与小数。

void uart_init()
{
    /* Clear the corresponding bit to be used. */
    UART1->CCR &= ~( UART_CCR_PEN_MASK | UART_CCR_PSEL_MASK | UART_CCR_SPB0_MASK | UART_CCR_SPB1_MASK | UART_CCR_CHAR_MASK );
    UART1->GCR &= ~( UART_GCR_AUTOFLOWEN_MASK | UART_GCR_RXEN_MASK | UART_GCR_TXEN_MASK );
    /* WordLength. */
    UART1->CCR |= UART_CCR_CHAR_MASK;
    /* XferMode. */
    UART1->GCR |= (UART_XferMode_RxTx << UART_GCR_RXEN_SHIFT);
    /* Setup baudrate, BOARD_DEBUG_UART_FREQ = 48000000u, BOARD_DEBUG_UART_BAUDRATE = 9600u. */
    UART1->BRR = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) / 16u;
    UART1->FRA = (BOARD_DEBUG_UART_FREQ / BOARD_DEBUG_UART_BAUDRATE) % 16u;
    /* Enable UART1. */
    UART1->GCR |= UART_GCR_UARTEN_MASK;
}

“图7.
图7. MM32F0140 UART_GCR寄存器

“图8.
图8. MM32F0140 UART_CCR寄存器部分位

UART发送数据 uart_putchar()

通过读取UART当前状态寄存器(UART_CSR)获取当前状态,当发送缓冲区为空时,可进行数据发送,将发送数据传入UART发送数据寄存器(UART_TDR),定义发送数据函数uart_putchar(),变量“c”为需要发送的数据。

void uart_putchar(uint8_t c)
{
    while ( 0u == ( UART_STATUS_TX_EMPTY & (UART1->CSR) ) )  /* Waiting tx buffer empty. */
    {}
    UART1->TDR = (uint8_t)c;
}

UART接收数据 uart_getchar()

通过读取UART当前状态寄存器(UART_CSR)获取当前状态,当接收缓冲接收了一个完整字节的数据时,可读取UART接收数据寄存器(UART_RDR)获取接收数据,定义接收数据函数uart_getchar(),该函数返回接收的数据。

uint8_t uart_getchar(void)
{
    while ( 0u == ( UART_STATUS_RX_DONE & (UART1->CSR) ) )  /* Waiting rx buffer receives a complete byte of data. */
    {}
    return (uint8_t)(UART1->RDR & 0xff);
}

UART输出字符串 uart_putbuffer()

使用UART发送函数编写发送字符串函数。

void uart_putbuffer(uint8_t *str)
{
    while ((*str) != '\0')
    {
        uart_putchar(*str);
        str++;
    }
}

main()函数

main()函数结合上述操作,不断循环接收数据函数uart_getchar()与发送数据函数uart_putchar(),将接收到的数据发送出去,实验现象如图9所示,程序运行后串口输出"uart_basic example.",通过串口调试工具输入"mindmotion",UART输出"mindmotion",输入数据与输出数据相同。

int main(void)
{
    uint8_t c;

    clock_init();
    enable_clock();
    pin_init();
    uart_init();

    uart_putbuffer((uint8_t *)"\r\nuart_basic example.\r\n");
    while (1)
    {
        c = uart_getchar();
        uart_putchar(c);
    }
}

“图9.
图9. 实验现象

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 83

EXTI简介

EXTI是外部中断/事件控制器,包含多个边沿检测器,通过检测I/O端口的电平变化判断是否产生中断/事件请求。

MM32F0140的EXTI包含19个外部中断线,其中外部中断线EXTI0 ~ EXTI15用于I/O映射,EXTI16连接到PVD输出,EXTI19连接到比较器1输出,EXTI24连接到IWDG中断。可通过软件控制任意一个I/O端口作为EXTI的输入源,EXTI检测对应端口是否产生边沿触发,若检测到边沿触发则产生中断/事件请求,GPIO对应的16个外部中断/事件映射关系如图1所示。

“图1
图1 MM32F0140 GPIO对应外部中断/事件映射

EXTI进行边沿检测时包含三种触发类型:

上升沿触发

电平由低到高时的一瞬间称为上升沿,由上升沿的产生触发输出变化就称作上升沿触发。

下降沿触发

电平由高到低时的一瞬间称为下降沿,由下降沿的产生触发输出变化则称为下降沿触发。

任意边沿触发

电平由上升沿或下降沿的产生而触发输出变化被称作任意边沿触发。

中断/事件产生过程如图2所示,EXTI边沿检测引脚的外部输入电平,若由于外部因素导致引脚电平变化并产生边沿触发,则边沿检测电路输出有效信号。或门电路接收边沿检测电路输出的信号与软件事件中断寄存器(EXTI_SWIER)的输出,软件事件中断寄存器能够通过软件启动中断/事件线。当外部中断线上触发边沿事件时,挂起寄存器(EXTI_PR)的对应位被置1,可通过读EXTI_PR寄存器获取当前中断/事件状态。或门电路的输出与中断屏蔽寄存器(EXTI_IMR)的输出相与,在使能对应线中断位且边沿触发有效信号时,输出有效信号到内核的NVIC中,NVIC进行中断处理。或门电路的输出与事件屏蔽寄存器(EXTI_EMR)的输出结果相与,当使能对应事件线位且边沿触发有效信号时,输出有效信号1,即脉冲信号,该脉冲信号可用于其他外设,例如触发TIM。

“图2
图2 EXTI模块框图

EXTI的配置

配置中断

配置并使能中断线,根据图1判断指定I/O端口对应的外部中断线与SYSCFG_EXTICRx寄存器中的控制位,(EXTI0~EXTI3使用SYSCFG_EXTICR1寄存器,EXTI4~EXTI7使用SYSCFG_EXTICR2寄存器,EXTI8~EXTI11使用SYSCFG_EXTICR3寄存器,EXTI12~EXTI15使用SYSCFG_EXTICR4寄存器)向SYSCFG_EXTICRx寄存器中外部中断线的对应位赋值,若使用PA管脚则对应位赋值为0000,PB管脚对应位赋值为0001,PC管脚对应位赋值为0010,PD管脚对应位赋值为0011。

配置边缘检测触发器的触发类型,若使用上升沿触发,则对上升沿触发选择寄存器(EXTI_RTSR)的外部中断线对应位置1;若使用下降沿触发,则对下降沿触发选择寄存器(EXTI_FTSR)的外部中断线对应位置1;若使用任意边沿触发,则EXTI_RTSR寄存器与EXTI_FTSR寄存器的外部中断线对应位均置1。

中断屏蔽寄存器(EXTI_IMR)的外部中断线对应位置1,允许中断请求。当指定的外部中断线检测到配置的触发条件时,产生一个中断请求,挂起寄存器(EXTI_PR)的对应位置1。通过软件对挂起寄存器中对应位写入1,使中断被清除。

配置软件中断事件寄存器(EXTI_SWIER)的外部中断线对应位为1并置1 EXTI_IMR寄存器的外部中断线对应位,也能产生中断。

配置事件

配置并使能事件线,对SYSCFG_EXTICRx寄存器中外部事件线的对应位赋值;配置边缘检测触发寄存器为需要的触发类型,对EXTI_RTSR寄存器与EXTI_FTSR寄存器赋值;事件屏蔽寄存器(EXTI_EMR)的对应位置1,允许事件请求。当检测到配置的触发条件时,产生一个事件请求,挂起寄存器对应位置 1;通过对挂起寄存器对应位写1清除事件。

配置软件中断事件寄存器(EXTI_SWIER)的外部中断/事件线对应位为1并置1 EXTI_EMR寄存器的对应位,也能产生事件。

实验

本实验在灵动官方开发板MB-023上进行,通过配置EXTI下降沿触发中断,按下按键后产生边沿触发,进行中断处理,LED电平转换。配置按键所使用的I/O端口的对应外部中断线,对SYSCFG_EXTICRx寄存器的EXTIx位赋值,对EXTI_RTSR寄存器和EXTI_FTSR寄存器赋值配置触发类型,使用EXTI_IMR寄存器使能中断,EXTI_PR寄存器对应位置1清除中断。具体实验内容为配置按键K2的对应引脚PB2与LED2对应的PB3引脚(如图3所示),配置PB2对应的外部中断线为下降沿触发,若按下K2,按键对应的端口输入低电平,下降沿触发,产生中断。实验现象为按下按键K2,LED2电平反转一次。

“图3
图3 MCU原理图中的EXTI引脚

初始化外设时钟

SYSCFG在APB2线上,GPIO在AHB线上,实验使用SYSCFG配置外部中断,按键K2与LED2的引脚均为GPIOB组的引脚。因此对RCC_APB2ENR寄存器的SYSCFGEN位置1,对RCC_AHBENR寄存器的GPIOB对应位置1,从而初始化外设时钟。

// Enable SYSCFG clock.
RCC->APB2ENR |= (1u << 0u);
// Enable GPIOB clock.
RCC->AHB1ENR |= (1u << 18u);

初始化按键

实验使用引脚为PB2的K2按键,按键原理图如图4所示,若K2按键按下则与GND导通,因此在初始化按键时需配置该端口的工作模式为上拉输入。

“图4
图4 原理图中的按键

GPIOx_CRL寄存器为端口配置低寄存器,用于配置指定端口的速度与工作模式;GPIOx_BSRR寄存器用于设置/清除对应端口,该寄存器低16位的对应端口位置1会产生高电平。由图5所示,K2所使用的端口PB2为GPIOx_CRL寄存器内第8 ~ 11位。

“图5
图5 GPIOx_CRL寄存器
// Clear the configuration bit of port 2.
GPIOB->CRL &= ~(0xf << 8u);
// Configure pull-up input mode.
GPIOB->CRL |= (0x8 << 8u);
// Configure PB2 pin to high level.
GPIOB->BSRR |= (1u << 2u);

初始化LED

实验使用PB3引脚,使用GPIOx_CRL寄存器对LED进行初始化配置,如图5所示,端口3为GPIOx_CRL寄存器内第12 ~ 15位。

// Clear the configuration bit of port 3.
GPIOB->CRL &= ~(0xf << 12u);
// Configure push-pull output mode.
GPIOB->CRL |= (0x1 << 12u);

配置中断线

由图1可知,PB2使用的外部中断线为EXTI2,配置SYSCFG_EXTICR1寄存器的EXTI2对应位为0001,如图6所示,EXTI2处于SYSCFG_EXTICR1寄存器的8~ 11位。

“图6
图6 GPIOx_EXTICR1
// Clear EXTI2 and assign value, the corresponding value of PB is 0001.
SYSCFG->EXTICR1 = ( ( SYSCFG->EXTICR1 & ~(0xf << 8u) ) | (0x1 << 8u) );

配置触发类型

在按键初始化中配置按键未按下时处于高电平,因此,对上升沿触发选择寄存器(EXTI_RTSR)与下降沿触发选择寄存器(EXTI_FTSR)赋值时配置触发类型为下降沿触发。

// Clear the corresponding bit of EXTI2 triggered by rising edge.
EXTI->RTSR &= ~ (1u << 2u);
// Configure falling edge trigger.
EXTI->FTSR |= (1u << 2u);

使能中断

配置EXTI_IMR寄存器的EXTI2对应位,使能中断。

// Enable EXTI interrupt.
EXTI->IMR |= (1u << 2u);

配置NVIC

EXTI控制中断,NVIC处理中断,使用Cortex-M0 core_cm0.h头文件中的NVIC_EnableIRQ使能中断线,EXTI2对应中断为EXTI2_3_IRQn。

// Setup NVIC.
NVIC_EnableIRQ (EXTI2_3_IRQn);

编写中断服务程序

中断使能中使用EXTI2_3_IRQn,中断处理函数要与其匹配,因此使用EXTI2_3_IRQHandler,设置变量app_exti_event_on作为中断状态标志,该变量初始时为false,中断请求产生时中断状态标志转换为true,将EXRI_PR寄存器的对应位写入1来清除中断。

void EXTI2_3_IRQHandler(void)
{
    uint32_t flags = EXTI->PR;
    if ( 0u != ( flags & (1u << 2u) ) )
    {
        app_exti_event_on = true;
    }

    EXTI->PR |= (1u << 2u); // Clear interrupt.
}

main()函数

主程序中初始化变量app_exti_event_times为0,设置该变量从0开始计数,当中断状态标志app_exti_event_on为true,即产生中断请求时,计数值加1,由于LED2初始化后显示为亮,计算计数值取余2,若余数不等于0则LED2灭,若余数为0则LED2亮。实验效果如图7所示。

int main(void)
{
    // Enable SYSCFG and GPIOB clock.
    RCC->APB2ENR |= (1u << 0u);
    RCC->AHB1ENR |= (1u << 18u);

    // Setup K2.
    GPIOB->CRL &= ~(0xf << 8u);
    GPIOB->CRL |= (0x8 << 8u);
    GPIOB->BSRR |= (1u << 2u);

    // Setup LED2.
    GPIOB->CRL &= ~(0xf << 12u);
    GPIOB->CRL |= (0x1 << 12u);

    // Setup SYSCFG EXTI2.
    SYSCFG->EXTICR1 = ( ( SYSCFG->EXTICR1 & ~(0xf << 8u) ) | (0x1 << 8u) );

    // Setup EXTI.
    EXTI->RTSR &= ~ (1u << 2u);
    EXTI->FTSR |= (1u << 2u);

    // Enable EXTI interrupt.
    EXTI->IMR |= (1u << 2u);

    // Setup NVIC.
    NVIC_EnableIRQ (EXTI2_3_IRQn);

    while (1)
    {
        while ( !app_exti_event_on )
        {
        }
        app_exti_event_on = false;

        app_exti_event_times++;
        if ( (app_exti_event_times % 2u) != 0u )
        {

            GPIOB->BSRR = (1u << 3u);// LED2(PB3 pin) off.
        }
        else
        {

            GPIOB->BRR = (1u << 3u);// LED2(PB3 pin) on.
        }
    }
}

“图7
图7 实验现象

来源: 灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 150

一、GPIO简介

GPIO作为通用输入输出端口,其全称为General Purpose Input Output,能够通过软件自由配置引脚状态及工作模式。除通用输入输出功能外,部分GPIO端口还可被用作第二功能的配置,即为复用功能。

每个通用I/O端口都可以通过软件自由配置为4种输入模式与4种输出模式。

输入模式

通过配置GPIOx_CRL寄存器或GPIOx_CRH寄存器中的MODEx[1:0]为00,即配置为输入模式,配置寄存器中的CNFx[1:0]选择工作模式(MM32F0140 的GPIOx中“x”的范围为A到D)。

“图1.
图1. 输入浮空/上拉输入/下拉输入/模拟输入配置

输入浮空:

浮空就是输入引脚即不接高电平也不接低电平,如图1所示,I/O端口的数据在每个AHB时钟被采样到输入数据寄存器,通过读访问输入数据寄存器获取当前I/O状态,输入浮空常用于复用功能。

上拉输入:

电阻与VDD相连,形成上拉电阻,I/O端口在空闲时为高电平,能够用于检测由高到低的电平变化,常用于按键检测。

下拉输入:

电阻与VSS相连,形成下拉电阻,I/O端口在空闲时为低电平,能够用于检测由低到高的电平变化。

模拟输入:

模拟输入是模拟信号的输入,在模拟输入模式下,上拉电阻、下拉电阻及斯密特触发器均被禁止。

输出模式

通过配置GPIOx_CRL寄存器或GPIOx_CRH寄存器中的CNFx[1:0]选择输出模式,配置GPIOx_CRL中的MODEx[1:0]选择输出速度(MODEx[1:0]不为00)。当I/O端口使用复用输出功能时,端口必须配置为复用功能输出模式(推挽或开漏),输出配置如图2所示。

“图2.
图2. 输出配置

开漏输出:

在开漏输出模式中,输出控制寄存器配置为0时,数据经过输出控制模块,MOS管的栅极接收到高电平,MOS管导通,此时I/O端口接通在GND上,即对应引脚输出低电平;当输出控制寄存器配置为1,数据经过控制模块,给予MOS管的栅极一个低电平,此时MOS管不导通,对应的管脚处于高阻态。因此,开漏输出通常输出低电平,若要输出高电平则需外加上拉电阻。

推挽输出:

推挽输出一般指两个MOS管分别受两个互补信号的控制,总是在一个MOS管导通时另一个MOS管截止。当输出控制寄存器配置为0时,数据经过输出控制模块,MOS管的栅极接收到高电平,N-MOS管导通,P-MOS管不导通,此时I/O端口接通在VSS上,即对应引脚输出为低电平;当输出控制寄存器配置为1时,数据经过输出控制模块,MOS管的栅极接收到低电平,P-MOS管导通,N-MOS管不导通,此时I/O端口接通在VDD上,即对应引脚输出为高电平。因此,推挽输出可以输出高低电平。

“图3.
图3. 复用功能配置

复用开漏输出:

配置GPIOx_AFRH与GPIOx_AFRL寄存器的AFRLx[3:0]与AFRHx[3:0]选择复用功能。当GPIO被作为第二功能使用时,模式配置为复用模式,复用功能配置如图3所示。以图4为例,若要配置PB10引脚作为I2C_SCL,则在配置GPIO模式时要选择复用模式。通过片上外设复用功能,使用MOS管实现输出。通常I2C使用GPIO的复用功能时会使用复用开漏输出模式,由于I2C的一个主设备可挂载多个从设备,若不使用复用开漏输出,而使用复用推挽输出,数据传输时,两个从设备一个拉高,一个拉低,可能会造成短路。因此I2C大多使用GPIO的复用开漏输出模式。

“图4.
图4. MM32F0140部分引脚复用功能

复用推挽输出:

当GPIO被作为第二功能配置使用时,模式需配置为复用模式,通过配置GPIO_AFRH与GPIOx_AFRL寄存器的AFRLx[3:0]与AFRHx[3:0]选择复用功能,通过片上外设复用功能,使用两个MOS管来实现输出。与推挽输出不同,复用推挽输出模式下端口的I/O操作由对应复用的功能模块控制,而推挽输出模式下控制I/O端口需对GPIO内部的寄存器进行操作。

二、配置GPIO

首先,使能对应I/O口的时钟,根据所使用的外设对RCC的RCC_AHBENR寄存器进行赋值,将对应外设位置1即可使能时钟,详细外设如图5所示。

“图5.
图5. MM32F0140 AHB外设

其次,配置所需的GPIO引脚、速度及工作模式。端口0到端口7使用GPIOx_CRL寄存器配置工作模式与速度,该寄存器中MODEx[1:0]位表示端口输入输出速度,CNFx[1:0]位表示端口工作模式(”MODEx”与”CNFx”的”x”表示指定端口号)。若配置GPIOx_CRL寄存器中的MODEx位等于00则端口为输入模式,此时CNFx位有四种配置方式,分别为:00(模拟输入模式),01(浮空输入模式),10(上拉/下拉输入模式);若MODEx位不为00,则对应端口为输出模式,此时CNFx具有四种配置方式:00(推挽输出模式),01(开漏输出模式),10(推挽复用模式),11(开漏复用模式)。端口8到端口15的配置使用GPIOx_CRH寄存器配置指定端口的工作模式与速度,详细配置方式与CPIOx_CRL寄存器相同。

若使用端口复用功能,需对GPIOx_AFRL(端口复用功能低位)寄存器与GPIOx_AFRH(端口复用功能高位)寄存器进行配置。端口号为0到7则使用GPIOx_AFRL寄存器,端口号为8到15则使用GPIOx_AFRH寄存器,根据端口号与复用功能表进行配置,例如若使用PA0引脚作为I2C1_SCL,则需将GPIOx_AFRL寄存器中AF3的对应位置1(GPIO的工作模式也要配置为复用模式),PA端口的复用功能表如图6所示。

“图6
图6 MM32F0140 PA端口复用功能表

三、实验

本实验通过使用GPIO获取按键状态控制LED亮灭,读取指定GPIO端口引脚的输入数据(读GPIOx_IDR寄存器)来获取当前的按键状态,通过对端口设置/清除寄存器(GPIOx_BSRR寄存器)与端口位清除寄存器(GPIOx_BRR寄存器)的对应端口赋值,使对应的LED亮灭。具体实验内容为配置PB3引脚对应LED2, PB4引脚对应LED3,PB2引脚对应的K2(如图7所示),若K2按下,则K2对应的端口输入低电平,设置实验现象为LED2灭、LED3亮,K2处于非按下时,K2对应的端口输入高电平,设置实验现象为LED2亮、LED3灭。

“图7
图7 引脚配置原理图

外设时钟初始化

GPIO在AHB线上,实验使用引脚均为GPIOB组的引脚,因此对RCC_AHBENR寄存器的GPIOB对应位置1。

RCC->AHB1ENR |= (1u << 18u);

按键初始化

实验使用引脚为PB2的K2按键,按键原理图如图8所示,若K2按键按下则与GND导通,因此在初始化按键时需配置该端口的工作模式为上拉输入。

“图8.
图8. 按键原理图

GPIOx_CRL寄存器为端口配置低寄存器,用于配置指定端口的速度与工作模式;GPIOx_BSRR寄存器用于设置/清除对应端口,该寄存器低16位的对应端口位置1会产生高电平。由图9所示,K2所使用的端口2为GPIOx_CRL寄存器内第8~11位。

“图9.端口配置低寄存器的比特位"
图9.端口配置低寄存器的比特位
//对应端口的配置涉及到4位,后两位配置端口输入输出速度,前两位配置工作模式;清零端口2的配置位
GPIOB->CRL &= ~(0xf << 8u); 

//速度配置位为0,端口为输入模式,上拉输入的工作模式位为10,因此使用0x08.
GPIOB->CRL |= (0x08 << 8u); 

//配置PB2引脚为高电平
GPIOB->BSRR |= (1u << 2u); 

LED初始化

实验使用PB3、PB4,引脚,为观察LED的高低电平,在配置工作模式时使用可以输出高低电平的推挽输出。因为PB3与PB4对应端口在端口0~7中,所以使用GPIOx_CRL寄存器对LED进行初始化配置(若使用端口为8~15则使用GPIOx_CRH寄存器)。

如图9所示,端口3为GPIOx_CRL寄存器内第12~15位,端口4为GPIOx_CRL寄存器内第16~19位。由于LED为低电平点亮,设置GPIOx_BSRR寄存器中LED2与LED3的对应位置1使LED初始状态为灭。

//复位将要使用的端口3与端口4的配置位
GPIOB->CRL &= ~( (0xf << 12u) | (0xf << 16u) ); 

//端口输入输出速度配置位不为00时,端口为输出模式,配置最大速度为50MHz,推挽输出模式的配置为00.
GPIOB->CRL |= ( (0x01 << 12u) | (0x01 << 16u) );

//PB3对应的LED2初始化状态为灭
GPIOB->BSRR = (1u << 3u); 

//PB4对应的LED3初始化状态为灭
GPIOB->BSRR = (1u << 4u); 

按键扫描

读GPIOx_IDR寄存器获取对应端口输入数据,本实验中K2配置为上拉输入,即按键未按下时为高电平,按下按键后,K2对应端口输入为低电平。若GPIOx_BSRR寄存器的低16位的对应端口位置1,则该端口为高电平;若GPIOx_BRR寄存器的对应端口位置1,则该端口为低电平。实验设置按键未按下时,LED2(PB3)亮、LED3(PB4)灭;按下按键时LED2灭、LED3亮。

while(1)
{
    //K2的引脚为PB2,  1u<<2u = 0100u,将PB2的对应位置1,若读出GPIOx_IDR的对应端口数据为高电平则按键未按下
    if ( 0u != ( GPIOB->IDR & (1u << 2u) ) ) 
    {
    //PB4引脚对应的LED3灭
        GPIOB->BSRR = (1u << 4u);

        //PB3引脚对应的LED2亮
        GPIOB->BRR = (1u << 3u); 
    }
    else  //K2 按键按下
    {
        //PB4引脚对应的LED3亮
        GPIOB->BRR =(1u << 4u);

        //PB3引脚对应的LED2灭
        GPIOB->BSRR = (1u << 3u);
    }
}

Main()函数

综合上述寄存器配置到main函数中,图10为实验效果。

int main(void)
{
    //GPIOB时钟初始化
    RCC->AHB1ENR |= (1u << 18u);

    //清零端口2的配置位
    GPIOB->CRL &= ~(0xfu << 8u);
    //端口2配置为上拉输入
    GPIOB->CRL |= (0x08u << 8u);
    //端口2配置为高电平
    GPIOB->BSRR |= (1u << 2u);

    //端口3与端口4配置位清零
    GPIOB->CRL &= ~( (0xf << 12u) | (0xf << 16u) );
    //端口3与端口4配置为推挽输出,速度最大为50MHz
    GPIOB->CRL |= ( (0x01 << 12u) | (0x01 << 16u) );

    //PB3对应的LED2初始化状态为灭
    GPIOB->BSRR = (1u << 3u);
    //PB4对应的LED3初始化状态为灭
    GPIOB->BSRR = (1u << 4u);

    while(1)
    {
        //K2的引脚为PB2,  1u << 2u = 0100u,将PB2的对应位置1,若读出GPIOx_IDR的对应端口数据为高电平则按键未按下
        if ( 0u != ( GPIOB->IDR & (1u << 2u) ) ) 
        {
            // PB4引脚对应的LED3灭
            GPIOB->BSRR = (1u << 4u);

            //PB3引脚对应的LED2亮
            GPIOB->BRR = (1u << 3u); 
        }
        else  //按下按键K2
        {
            // PB4引脚对应的LED3亮
            GPIOB->BRR = (1u << 4u);

            // PB3引脚对应的LED2灭
            GPIOB->BSRR = (1u << 3u); 
        }
    }
}

“图9.实验效果"
图9.实验效果

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 67

页面

订阅 RSS - MM32F0140