2024年寒假练-带调试器的i.MX RT1021开发板实现温湿度测量系统&自动恒温系统
该项目使用了带调试器的i.MX RT1021开发板,实现了温湿度测量系统&自动恒温系统的设计,它的主要功能为:温湿度测量和加热控制。
标签
Keil
RT1021
2024寒假练
唉可悲
更新2024-03-29
四川航天职业技术学院
245

1.项目需求

本项目制作的是2024年“寒假在家一起练”平台5带调试器的i.MX RT1021开发板的任务,项目要求如下:

板卡的核心板下方有一颗温湿度传感器,并在传感器旁配有加热电阻,可通过旋钮选择功能:

1)实现温湿度自动采集,并显示在屏幕上;

2)实现恒温控制功能,进入恒温菜单后,可以实现温度设定,进入工作状态后,能达到设定温度,并显示当前温度曲线。

2.完成的功能

1. 实现温湿度自动采集,并显示在屏幕上;

2. 能够进行温度的控制,通过旋钮旋转可以设定需求温度,按下旋钮则可以启动或者停止加热,温度控制采用久经考验的PID控制;

3. 移植了lvgl,并采用曲线形式显示当前温度和设定温度,并且图表可以自动根据数据更新显示范围,方便观察。

3.未实现功能或计划实现功能

1.触屏功能(触摸时xt2046 pin引脚无法被拉低);

2.磁力传感器控制屏幕方向(数据可以获得,但是有较大问题);

3.DMA传输实现。

4.主要实现思路

i.MX RT1021开发板搭载的MIMXRT1021CAG4A芯片,基于Arm®Cortex®-M7内核,运行频率高达500MHz,内置256KB片上RAM,本质上也是ARM系列芯片,因此开发环境采用最普遍的keil环境,之所以不使用官方IDE,原因是因为是在太卡了,而且只能调试,不能下载(也就是不能一步下载进去),由于该芯片没有内置flash,必须使用外置的,下载速度相比stm32流内置flash的芯片慢很多,因此不能一步实现下载很浪费时间。

这颗芯片的性能很强,且官方也配套有IDE和keil包,开发不是很难,最关键的是官方还配有MCUXpresso Config Tools能够和stm32芯片的cubemx一样实现图形化配置非常方便,因此本项目采用MCUXpresso Config Tools进行配置,使用keil进行开发。

由于是ARM芯片,虽然官方已经对freertos进行了完整适配,但是我还是倾向于使用RTX5这个操作系统,能和keil结合紧密并且可以实现零中断切换,还是很舒服的。

 

整个项目使用RTX5创建了三个线程,分别用来扫描按键,LCD绘制,温度监测,使用操作系统相比裸机更方便延时,不用考虑柱塞问题。具体的运行流程图如下所示。

 

5.主要实现步骤说明

本项目直接创建于hello_world例程(也算是单片机万物起源,哈哈哈)

1.MCUXpresso Config Tools配置要点

1.1 I2C引脚配置。

I2C引脚中的software Input On必须设成Enable,否则可能不能使用。

 

1.2 所有引脚配置

所有引脚的方向尽量指定好,虽然可能没问题,但是有可能出错。

 

1.3 I2C配置

 

I2C配置相对简单,速度设定为100KHZ常规即可

1.4 SPI配置

 

SPI设置也没什么注意的,速度拉高即可,因为这个st7735理论能支持很高速度,我设定的50M,虽然芯片不一定能跑到。

1.5 PWM配置

PWM配置请一定注意,我的血泪史,sm设置的只是名字,不重复就行,频率可以设高点,但是不能太高,加热采用的mos管应该有上限的频率,高点控制更加精准。

 

注意下图,这个mos管连接B通道,注意初始占空比应该为0,且有效输出为High,由于加热模块速度非常快,一定要谨慎开启避免出事,我开始时一直都是拿LED灯做实验,就是为了避免出问题。重点来了,下图3编号,一定一定要选一个,这个是rt1021芯片特点,当出现错误输出时关闭PWM保证安全,之前我觉得无所谓这个保险,就没选,结果PWM不输出.....图4的话就是出故障输出什么电平,图5就是故障检测的一些配置,比较简单。

 

2.功能实现-LVGL移植

本功能主要参考了lvgl8.3移植到stm32(https://blog.csdn.net/qq_59953808/article/details/126445608)

移植实际上比较容易,由于触摸没有调通,这里只使用了显示,由于用到了RTOS,因此lvgl内存和RTOS内存可能会挤爆默认的128kb使用内容,实际上rt1021有256kb内存,因此需要对分配文件进行修改,实际上keil中修改是不行的,需要修改MIMXRT1021\arm目录下文件,我keil采用flexspi_nor_release作为编译目标,因此修改MIMXRT1021xxxxx_flexspi_nor这个文件,在文件结尾地方,将lvgl和rtos内存的文件放在这个内存地方,至于怎么找到这两个o文件,可以参考keil编译生成的map文件即可。默认是放在RW_m_data里面。

  RW_m_data m_data_start m_data_size-Stack_Size-Heap_Size { ; RW data
.ANY (+RW +ZI)
* (RamFunction)
* (NonCacheable.init)
* (*NonCacheable)
* (DataQuickAccess)
}

ARM_LIB_HEAP +0 EMPTY Heap_Size { ; Heap region growing up
}
ARM_LIB_STACK m_data_start+m_data_size EMPTY -Stack_Size { ; Stack region growing down
}
RW_m_ram_text m_qacode_start m_qacode_size { ;
* (CodeQuickAccess)
lv_mem.o(+RW +ZI)
rtx_lib.o(+RW +ZI)
}
RW_m_ncache m_data2_start m_data2_size {

}
RW_m_ncache_unused +0 EMPTY m_data2_size-ImageLength(RW_m_ncache) { ; Empty region added for MPU configuration
}

Lvgl还有需要说明的地方,为了实现快速刷屏,arm芯片普遍都是小端(大小端自行百度),因此需要打开#define LV_COLOR_16_SWAP 1

快速刷屏需要一次性传入color数据,我自己写了一个函数,感兴趣的朋友可以下载源代码查看。

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    if(disp_flush_enabled) {
        /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
                lvgl_LCD_Fill(area->x1, area->y1, area->x2, area->y2, color_p);
    }

    /*IMPORTANT!!!
     *Inform the graphics library that you are ready with the flushing*/
    lv_disp_flush_ready(disp_drv);
}



void lvgl_LCD_Fill(u16 sx, u16 sy, u16 ex, u16 ey, lv_color_t *color)
{
    u32 xysize = (ey - sy + 1) * (ex - sx + 1) * 2;
    LCD_SetWindows(sx, sy, ex, ey);
    LCD_RS_SET;
    SPI_WriteByte_more((uint8_t *)color, xysize);
}

void SPI_WriteByte_more(u8 *data,int num)
{
    masterXfer.txData = data;
    masterXfer.rxData = NULL;
    masterXfer.dataSize = num;
    masterXfer.configFlags = EXAMPLE_LPSPI_MASTER_PCS_FOR_TRANSFER | kLPSPI_MasterByteSwap| kLPSPI_MasterPcsContinuous;

    LPSPI_MasterTransferBlocking(EXAMPLE_LPSPI_MASTER_BASEADDR, &masterXfer);
}

3.功能实现-NSHT30温湿度传感器

温湿度传感器实际上很简单,我直接移植的SHT30的相关程序,但是需要注意两点

1. NSHT30是国产,和SHT30命令不同,需要针对性修改,最开始没注意,一直出错....

2. I2C地址库函数会自动处理地址首位的发送或接收位,不用也不能自己进行修改,传输地址一直都应该是默认值0x44

下图是封装后的I2C读取的函数,可以下载源代码自行查看。

bool I2C_ReadBuffer(uint8_t SalveAddr, uint16_t RegAddr, uint8_t *DateByte, uint32_t DataNum)
{

    lpi2c_master_transfer_t transfer;
    status_t err_flag;

    /*
     * @data         :发送、接受的数据
     * @datasize     :发送的数据个数
     * @direction    :读写模式选择
     * @flags        :传输失败的标志位
     * @slaveAaddress:从机地址
     * @subaddress   :寄存器/内存地址
     * @subaddressSize:地址寄存器大小
     */
    transfer.data = DateByte;
    transfer.dataSize = DataNum;
    transfer.direction = kLPI2C_Read;
    transfer.flags = kLPI2C_TransferDefaultFlag;
    transfer.slaveAddress = SalveAddr;
    transfer.subaddress = RegAddr;
    transfer.subaddressSize = 0x02;
    err_flag = LPI2C_MasterTransferBlocking(LPI2C1, &transfer);

    if (err_flag != kStatus_Success)
        return false;

    return true;
}
 

4.功能实现-PWM+PID控温

首先说下PWM的问题,pwm设定占空比一句函数就够,但是不会起作用,必须要有另外一句函数一起才有作用。图中1是设定占空比,其中一个是灯光的,一个是加热的,实现灯光与加热同步,方便观察。2就是生效函数,必须有。

PWM_UpdatePwmDutycycle(PWM2_PERIPHERAL, PWM2_SM0, PWM2_SM0_A, kPWM_CenterAligned ,output);  《《1》》
PWM_UpdatePwmDutycycle(PWM2_PERIPHERAL, PWM2_SM3, PWM2_SM3_B, kPWM_CenterAligned ,output); 《《1》》
PWM_SetPwmLdok(PWM2_PERIPHERAL, (kPWM_Control_Module_0 | kPWM_Control_Module_3), true); ​《《2》》

PID需要采用位置式PID,最开始让chatgpt帮忙写,结果不能运行,还是需要自己进行移植,需要注意的是占空比只能0-100(当然高精度可以0-65535,但是没必要),因此需要限幅。

if (PID_Para.pid_result > 100)
     PID_Para.pid_result = 100; // 占空比 0-100
else if (PID_Para.pid_result < 0)
     PID_Para.pid_result = 0;

可以在函数这里设定初始值,实际上这个pid有点难调,我反正调了很久也不满意,有机会尝试下串口输入调整,方便点,I_MAX是积分限幅,能避免过冲,但是可能导致不能达到预定值。

#define Kp 90 // 比例常数  100倍值    100即1
#define Ki 60 // 积分常数  100倍值   18即0.18
#define Kd 50 // 微分常数  100倍值    90即0.9

#define I_MAX 3500
#define I_MIN 0

温控流程还是简单,就是不断PID然后用输出控制占空比,如果停止或者SHT30故障占空比设为0即可。

5.功能实现-按键和旋钮

按键和旋钮实现在mysys.c文件中,按键采用状态机进行实现,网上也有很多实现和例子,我使用的我用了几年的实现,自己也优化了一些内容,可以自行查看源代码。这里只用了EC11上的一颗按钮。

void Key_Deal(void) //按键扫描程序
{
    for (int i = 0; i < 1; i++)
    {

        switch (Key[i].KeyLogic) {
            case KEY_OFF:
                if (Key[i].KeyPhysic == KEY_ON && Key[i].KeyONCounts < SHAKES_COUNTS) {
                    Key[i].KeyONCounts++; //
                } else if (Key[i].KeyPhysic == KEY_OFF && Key[i].KeyONCounts < SHAKES_COUNTS) {
                    Key[i].KeyONCounts = 0;
                } else if (Key[i].KeyPhysic == KEY_ON && Key[i].KeyONCounts == SHAKES_COUNTS) {
                    Key[i].KeyLogic = KEY_IDLE;
                }
                break;
            case KEY_IDLE:
                if (Key[i].KeyPhysic == KEY_ON && Key[i].KeyONCounts < HOLD_COUNTS) {
                    Key[i].KeyONCounts++; // aa
                } else if (Key[i].KeyPhysic == KEY_OFF && Key[i].KeyOFFCounts < SHAKES_COUNTS) {
                    Key[i].KeyOFFCounts++;
                } else if (Key[i].KeyPhysic == KEY_OFF && Key[i].KeyONCounts < HOLD_COUNTS && Key[i].KeyOFFCounts == SHAKES_COUNTS) {
                    Key[i].KeyLogic    = KEY_ON;
                    Key[i].KeyONCounts = 0;
                } else if (Key[i].KeyONCounts >= HOLD_COUNTS) {
                    Key[i].KeyLogic    = KEY_HOLD;
                    Key[i].KeyONCounts = 0;
                }
                break;
            case KEY_ON:
                if (Key[i].KeyPhysic == KEY_OFF && Key[i].KeyOFFCounts < SHAKES_COUNTS_2) {
                    Key[i].KeyOFFCounts++;
                } else if (Key[i].KeyPhysic == KEY_OFF && Key[i].KeyOFFCounts == SHAKES_COUNTS_2) {
                    Key[i].KeyLogic     = KEY_OFF;
                    Key[i].KeyOFFCounts = 0;
                    Key[i].KeyIsDeal    = 0;
                }
                break;
            case KEY_HOLD:
                if (Key[i].KeyPhysic == KEY_OFF && Key[i].KeyOFFCounts < SHAKES_COUNTS_2) {
                    Key[i].KeyOFFCounts++;
                } else if (Key[i].KeyPhysic == KEY_OFF && Key[i].KeyOFFCounts == SHAKES_COUNTS_2) {
                    Key[i].KeyLogic     = KEY_OFF;
                    Key[i].KeyOFFCounts = 0;
                    Key[i].KeyIsDeal    = 0;
                }
                break;
            default:
                break;
        }
    }
}

旋钮是EC11旋钮,采用的也是之前用过的

char EC11_A_Last = 0; // EC11的A引脚上一次的状态
char EC11_B_Last = 0; // EC11的B引脚上一次的状态
void EC11_sc()
{
    static char EC11_A_Last = 0;                             // EC11的A引脚上一次的状态
    if (GPIO_PinRead(GPIO2, 9) != EC11_A_Last)                   // 以A为时钟,B为数据。正转时AB反相,反转时AB同相
    {
        if (GPIO_PinRead(GPIO2, 9) == 0)
        {
            if (Node.NodeIsDeal == 0)
            {
                if (GPIO_PinRead(GPIO2, 7) == 1) // 只需要采集A的上升沿或下降沿的任意一个状态,若A下降沿时B为1,正转
                {
                    Node.NodeLogic = 1;
                    Node.NodeIsDeal = 1;
                }
                else // 反转
                {
                    Node.NodeLogic = 2;
                    Node.NodeIsDeal = 1;
                }
            }
        }
        EC11_A_Last = GPIO_PinRead(GPIO2, 9); // 更新编码器上一个状态暂存变量
    }
}

需要注意的是,按钮可以5ms一次扫描,但是旋钮需要2ms一次,不然容易出错。

6.遇到的问题和解决

1.在编写程序时采用keil编程时,出现了下载器不能使用的问题,原本以为是脚本问题,结果不是,换了另外一个dap就可以下载,后面使用。i.MX的linksever凑合了一段时间,但是实在不爽,怀疑是rp2040移植的daplink有问题,后面再网上找了开源代码,简单修改了下,可以实现keil下载,需要的朋友可以下载uf2文件,按住下载器上的按钮进入u盘模式,把uf2文件拖进去即可。

2.PWM困扰了我一周时间,原因和解决都在上面说过,只能说不同芯片差距很大,不能用原有思想去衡量。

3.NSHT30也是大坑,寄存器命令不兼容,困扰了一段时间,I2C也需要注意,不像其他32芯片需要自己计算传入传出,库函数帮你进行了计算,不知是好是坏。

7.总结和注意

很感谢硬禾学堂提供的这个平台和机会,只有实践才能促进进步,只有实践才能发现问题并解决问题!

这次由于自己比较忙,实际上是有摄像头,但是确实没有时间折腾了,所以只能做个比较简单的(当然还是很麻烦踩坑)

这个复刻比较容易,可以直接下载hex进去即可,可以采用NXP-MCUBootUtility工具下载即可。

8.实物照片

 

 

 

 

物料清单
附件下载
源程序.7z
源程序,采用keil编写,MCUXpresso Configv15配置
rt1021_yqxt_1.hex
本项目hex文件
dap_rp2040.uf2
可以用keil下载的调试器固件(可以串口通讯)
团队介绍
四川航天职业技术学院
团队成员
唉可悲
电子爱好者
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号