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.实物照片