【IIC】IIC中断与DMA&状态机编程

上次,我们一起学习了如何使用IIC的普通轮询模式,与AHT20模块进行通信,测量了房间的温湿度。这次,我们一起来看看IIC的中断与DMA。

我们不妨来回顾一下,上次的学习中,我们使用HAL_I2C_Master_Transmit向AHT20发送了触发测量的指令。就像我们介绍的那样,主机在向IIC从机发送(写)指令的时候,设备地址的最后一位为0,而从机AHT20接收到指令后,也不能回复任何数据,只能回复ACK信号表明自己已经接收到指令,随后我们又使用HAL_I2C_Master_Receive请求AHT20的测量数据,按照IIC协议,主机向从机请求(读)数据时,只能发送设备地址,而且设备地址的最后一位为1,当然0x700x71的转换由HAL库帮我们完成,读取请求中只有地址,不能携带任何数据,随后从机回复ACK后,就开始回复固定字节数量的数据,至于多少字节数,通常与上次发送的指令有关,具体的工程中需要查询具体模块的手册。从机每发送一个字节后,都需要主机发送一次ACK信号,代表已经接收到这一字节数据。

与我们之前详细讲解过的串口轮询模式的发送流程类似,IIC轮询模式的发送也需要由软件逐个字节去控制相关寄存器,发完一个字节在发送下一个,直到全部完成才能继续执行接下来的代码。

轮询模式的接收也是类似的过程,整个发送或者接收过程中,一直堵塞执行,占用着CPU资源。为了解决此问题,IIC也有中断模式与DMA模式。

与串口一样,IIC的中断模式发送也是将一字节数据塞入寄存器后,CPU继续执行正常任务,直到此字节数据发送完成后产生中断,CPU再来塞入下一字节数据,IIC中断模式接收也是类似。搬运完一字节数据后就去执行其他任务,直到下一字节数据到来,继续搬运。而DMA模式下CPU更加省力,整个发送或者接收的数据都交给小助手DMA去搬运,等数据发送或者接收完成,再由DMA通过中断通知CPU前来处理。

使用起来IIC的中断与DMA模式其实也是跟串口的这两种模式用法一样。来到我们上次创建的iic工程,首先在Cube MX中打开IIC1的两个中断向量,一个是IIC事件(event)中断,一个是IIC错误(error)中断。

我们这次主要用的是这个事件中断向量,Ctrl+S保存并生成代码,main.c里依旧是我们上次写的代码。while循环里是调用AHT20_Read获取温度与湿度,然后用串口输出出来。我们就一起试试用IIC的中断模式改造一下AHT20_Read函数吧。

为了防止内存上的旧数据影响一会的实验效果,我们先给readBuffer初始化一下,然后开始改造。首先是发送函数Transmit,仅需在Transmit后添加_IT即可。另外由于是非阻塞函数,运行瞬间就能完成,后续的步骤都交给了中断机制,因而也就不存在等待时间这么一个参数了。接收函数Receive的改造也是类似,添加_IT以及删掉等待时间参数,这就改造好了,但是总感觉哪里不对。先编译下载看一下吧。打开串口调试助手看一下效果。

可以看到数据出现了错误,这是怎么回事呢?其实,这时由于非阻塞函数的特性造成的,早在学习串口时我们就了解过轮询模式是阻塞模式。程序会等到所有的数据发送/接收完成才会接着向下执行,而中断与DMA模式则是非阻塞模式,他们将任务交给外设后就会接着向下执行,并不会等待数据的发送/接收完成 ,整个通信流程都交给外设与中断进行控制,通信完成后,再通过中断来通知我们,所以我们在此对readBuffer进行解析时,上面这行Receive读取的数据只是通知外设进行了读取,但尚未读取完成。readBuffer里的数据还都是我们初始化的0,解析出来的数据当然也就不正确了。

复制代码
void AHT20_Read(float *Temperatuer, float *Humidity)
{
    uint8_t sendBuffer[3] = {0xAC, 0x33, 0x00 };
     uint8_t readBuffer[6] = {0};
    
    HAL_I2C_Master_Transmit_IT(&hi2c1, AHT20_ADDRESS, sendBuffer, 3);
    HAL_Delay(75);
    HAL_I2C_Master_Receive_IT(&hi2c1, AHT20_ADDRESS, readBuffer, 6);
    if((readBuffer[0] & 0x80) == 0x00)
    {
        uint32_t data = 0;
        data = ((uint32_t)readBuffer[3] >> 4) + ((uint32_t)readBuffer[2] << 4) + ((uint32_t)readBuffer[1] << 12);
        *Humidity = data * 100.0f /(1 << 20);

        data = ((uint32_t)readBuffer[3] << 16) + ((uint32_t)readBuffer[4] << 8) + (uint32_t)readBuffer[5] ;
        *Temperatuer = data * 200.0f / (1 << 20) - 50;
    }
}

所以,我们得来进行一波改造,搞一个简单的状态机出来,不同的状态下做不同的事。那么首先我们把发送测量指令与数据读取、数据解析都拆分开,第一个就叫做AHT20_Measure只管发送测量指令,需要注意的是,我们将sendBuffer数组的指针传入了中断发送函数,中断发送函数回在后续的执行流程中使用它,但是sendBuffer变量在AHT20_Measure函数执行完成后就不存在了,占用的内存地址也会被回收,分配给其他变量使用,这样的话,这块内存地址上存的数据也可能会被修改,造成发送的数据有误,所以我们应该将sendBuffer设置为static,这样sendBuffer就会一直占用这块内存,即使是AHT20_Measure函数执行结束,也不会被释放回收了。

复制代码
void AHT20_Measure()
{
    static uint8_t sendBuffer[3] = {0xAC, 0x33, 0x00 };
    HAL_I2C_Master_Transmit_IT(&hi2c1, AHT20_ADDRESS, sendBuffer, 3);
}

还有一个函数AHT20_Get,此函数用于读取AHT20的测量数据,由于我们接下来的另一个函数也需要使用这个readBuffer数组,所以我们将其提升为全局变量,全局变量最好放在所有函数的上面。

复制代码
uint8_t readBuffer[6] = {0};

void AHT20_Get()
{
    HAL_I2C_Master_Receive_IT(&hi2c1, AHT20_ADDRESS, readBuffer, 6);
}

然后我们来写解析数据的函数AHT20_Analysis,要有两个用于向外传递数据的float指针入参,然后还是复制已经完成的解析。

复制代码
void AHT20_Analysis(float *Temperature, float *Humidity)
{
    if((readBuffer[0] & 0x80) == 0x00)
    {
        uint32_t data = 0;
        data = ((uint32_t)readBuffer[3] >> 4) + ((uint32_t)readBuffer[2] << 4) + ((uint32_t)readBuffer[1] << 12);
        *Humidity = data * 100.0f /(1 << 20);

        data = ((uint32_t)readBuffer[3] << 16) + ((uint32_t)readBuffer[4] << 8) + (uint32_t)readBuffer[5] ;
        *Temperatuer = data * 200.0f / (1 << 20) - 50;
    }
}

OK,已经全部分开了。 为了能在别的文件中使用,我们将它们在aht20.h在进行声明。

随后我们来到main.c中,修改一下整个aht20驱动的使用流程,首先在用来写我们自己变量的PV注释对中,定义一个aht20State变量,用来记录状态机的状态,可以写个注释来规定一下各个状态值,0是初始状态,这个状态下发送测量命令,1是正在发送测量命令,2是测量命令发送完成,这个状态下要等待75ms,然后进行IIC读取,3是IIC读取中,4是读取完成了,那么就要进行数据的解析与输出。如此进行简单状态循环,就是我们本次要实现的状态机。

对了,这里我们使用的是中文注释,在Cube IDE里使用中文有一点是一定要注意的,就是使用Cube IDE重新生成代码后,中文会乱码。这是由于编码不一致造成的。我们需要搜索打开Windows的环境变量,新建一个系统变量,变量名为JAVA_TOOL_OPTIONS,变量值设置为-Dfile.encoding=UTF-8,确定之后重新启动Cube IDE就不会出现这种情况了。

回到代码,来到while死循环中,将原来的代码注释掉,我们来写状态机的代码。首先我们不妨先把所有状态情况的判断写出来

复制代码
while(1)
{
    if(aht20State == 0)
    {
        
    }
    else if(aht20State == 1)
    {
        
    }
    else if(aht20State == 2)
    {
        
    }
    else if(aht20State == 3)
    {
        
    }
    else if(aht20State == 4)
    {
        
    }

}

然后我们挨个补充每个状态机下的动作,首先是aht20State为0时的初始状态,此状态下我们使用AHT20_Measure触发测量,然后将状态机设置为正在发生测量命令,也就是1,而正在发送测量命令时,我们不需要做什么,所以我们跳过,直接来写测量指令发送完成时的动作,首先延迟75ms,随后调用AHT20_Get函数去读取AHT20数据。然后将状态设置为正在读取中3,读取中也不需要进行任何操作,所以跳过。接下来是状态值为4时,数据读取完成,进行数据的分析。将我们的温湿度变量传入AHT20数据分析函数,并且依旧使用串口发送出来,随后延迟个1秒钟,把状态值再恢复为初始状态0。OK好像是写完了。

复制代码
while(1)
{
    if(aht20State == 0)
    {
        AHT20_Measure();
        aht20State = 1;
    }
    else if(aht20State == 2)
    {
        HAL_Delay(75);
        HAT20_Get();
        aht20State = 3;
    }
    else if(aht20State == 4)
    {
        AHT20_Analysis(&temperature, &humidity);
        sprintf(message,"温度:%.1f ℃, 湿度:%.1f %%\r\n",temperature, humidity);
        HAL_UART_Transmit(&huart2, (uint8_t)message,strlen(message),HAL_MAX_Delay );
        HAL_Delay(1000);
        aht20State = 0;
    }

}

不过我们会发现,并没有将状态值从1设置为2和从3设置为4的语句。这是因为在整个流程中我们好像并不知道IIC何时完成了发送,也不知道何时完成了接收。回想我们在学习串口的中断模式时,HAL库是通过调用我们重新实现的HAL_UART_RxCpltCallback函数来通知我们串口接收完成的,那么IIC是否也有类似的函数呢?

答案是有的,我们找到stm32f1xx_hal_i2c.c文件打开,出现这个提示,是因为此文件比较大,点击yes,

然后把这个数字改大一点,这个勾勾去掉就好了

我们从大纲视图中仔细找找,就可以找到HAL_I2C_MasterTxCpltCallback以及HAL_I2C_MasterRxCpltCallback,全部带有__weak标记顾名思义TxCpltCallback就是发送完成回调,RxCpltCallback就是接收(读取)完成回调,我们把它们在i2c.c文件中重新实现一下。

因为接下来我们需要在i2c.c中调用main.c中定义的aht20State变量,所以我们将此变量在main.h中进行extern声明,随后回到i2c.c。首先是TxCpltCallback,此函数会在通过HAL_I2C_Master_Transmit_IT函数发送的命令数据全部送达从机后,由I2C中断调用。首先我们先象征性地判断一下进入这个中断回调的是不是i2c1,说是象征性的,是因为我们其实也没有使用其他i2c,这样写只是为了保持一个好习惯。

然后,由于我们只会通过中断发送测量指令,所以直接将状态机状态设置为发送完成,也就是2。RxCpltCallback也是同理。它会在我们通过HAL_I2C_Master_Receive_IT向从机请求的数据全部接受完成后,由I2C中断调用。我们也是先判断一下是不是i2c1进入的中断,然后将状态设置为接收完成,这下一个状态机的完整循环就搭建好啦。

复制代码
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if(hi2c == &hi2c1)
    {
        aht20State = 2;
    }
}

void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if(hi2c == &hi2c1)
    {
        aht20State = 4;
    }
}

最后编译下载一气呵成。来到串口助手,又能准确地测量温湿度数据了

那么刚刚我们使用的是IIC的中断模式,虽然已经不在阻塞执行,省出来不少CPU时间,但是还记得我们的小助手DMA吗?它可以替CPU搬运数据,省出更多的CPU时间,而且使用起来也非常的简单。

回到Cube MX界面,来到I2C1 DMA Settings,跟串口一样,添加上发送和接收的DMA通道,默认配置就是常用配置,所以也不需要进行改动。

来到NVIC Settings可以看到多了DMA相关的中断

保存并生成代码。DMA模式也跟中断模式一样,也是非阻塞模式,而且也是通过中断调用这两个Callback函数通知我们,只是多了个帮忙搬运数据的小助手而已,所以代码上不需要做多少修改,只需要到aht20.c中,把TransmitReceive函数后的_IT改成_DMA,编译下载看一下效果。

江湖传言:"STM32的硬件IIC有Bug,所以最好使用软件模拟IIC"

所谓软件模拟IIC,其实就是自己通过代码控制GPIO口,去生产和读取SDA与SCL的电平变化。其实,自己动手实现一遍模拟IIC,对大家加深对IIC的理解非常有帮助,不过针对此江湖传言大家也不必太过在意,此问题是ST公司在STM32F10系列芯片中,硬件IIC的一些设计缺陷与特性产生的,而早些年的标准库比较简陋,并未进行太多优化。如今HAL一直在不断优化更新,尽最大可能地在软件层面上规避了这些问题。

本次我们不仅学习使用了IIC的中断与DMA模式,更重要的是,我们感受到了单片机中非常非常常见的一种编程方式,状态机编程。后续大家也会发现,在稍微复杂的项目中,处处都是状态机的身影.

相关推荐
蓝斯49710 小时前
从.到实现一个Windows上的虚拟hid键盘设备
windows·单片机·计算机外设
熙芯XiChip11 小时前
硬实时与软实时RTOS核心区别
嵌入式硬件
伟大的前程11 小时前
unsigned int 定义的变量 在 电脑的 64 位系统中 和 32 位 cortex-m0 和 8 位的 51 单片机
单片机·嵌入式硬件
小渔村的拉线工12 小时前
1.HPM6E80 解析芯片整体工作原理和存储架构
单片机·嵌入式硬件·mcu·架构·hpm6e80
熙芯XiChip12 小时前
CAN与CANopen的本质区别
单片机
lilihewo13 小时前
Altium Designer 封装库的创建(分享pcb 3D封装库)
嵌入式硬件·51单片机
神罚天下ET14 小时前
典型arm位单片机启动流程(从上电到main.c)
c语言·arm开发·单片机
碧海银沙音频科技研究院15 小时前
蓝牙耳机使用WSDF5361锂保芯片进入船运模式实现方法
嵌入式硬件·算法
Zevalin爱灰灰15 小时前
模拟信号链电路——基于TL431的电压基准电路
单片机·嵌入式硬件
csdn杰哥16 小时前
瑞萨单片机开发教程[二十七] DTC 外部中断触发传输实验
单片机·嵌入式硬件·mongodb·瑞萨·ra6m5