任务介绍
本项目实现了2024寒假练中,i.MX RT1021开发板的任务4:实现亮度测量与控制,在RT1021开发板上,使用光强传感器,实现了基于Gamma矫正的LCD屏幕背光自动控制。
硬件平台
首先介绍本期活动的主角,恩智浦的i.MX RT1021开发板,它的主控是一颗型号i.MX RT1021的微控制器,核心是ARM Cortex-M7,运行频率高达500MHz,可以说性能很强。这是一块相对小巧的开发板,但是我们做电子设计常用的资源基本都搭载到了,除了三色灯、触摸屏、旋钮、摇杆这些交互元件,还搭载了温湿度、光强、磁场、加速度、角速度传感器,可以应用到很多场景。
相比于同系列的RT1052、RT1064,i.MX RT1021的资源基本通用,只是SRAM和一些外设的数量相对少一些,通过这块开发板,我们可以练习到i.MX RT系列的大部分功能,比如USB、SPI、I2C这些通信协议,和PWM、ADC、定时器、SD卡这些外设,硬禾这次寒假练活动是一次非常好的学习机会。
任务分析与实现
接下来我们来简单分析一下,这次主办方出的任务:
- 任务一是比较常规的传感器读取显示,考察对各类总线协议的掌握
- 从任务二和三开始引入了USB协议栈,总的来说是通过板子上的传感器、旋钮、摇杆等设备来控制电脑
- 任务四是实现亮度测量与控制,主要涉及光强传感器、LCD和PWM这三个外设
- 任务五和四类似,涉及温湿度传感器,同时需要加一点控制算法
- 任务六和八比较偏音视频处理
- 任务七需要读取IMU传感器,并和上位机进行交互
这次我选择了任务四:亮度测量与控制,整个系统框图如下,从左到右整体呈一个输入到输出的关系:主控读取触摸芯片和光强传感器的数据,实现LCD触屏的交互,以及自动调整背光。
这一过程主要涉及I2C、SPI、PWM三种外设。我们接下来逐个实现这些功能。这次实现的整体方案是RT-Thread软件平台,因为我感觉相比使用官方SDK这种和芯片强绑定的开发方式,通过RT-Thread开发积累下来的代码和组件可复用性更强,以后哪怕换了一个芯片和开发板,也很容易把本次写的代码用起来,减少重复劳动。另一方面,这次我适配了一个新的BH1730外设驱动,任务完成后,将自己写的代码开源出去,以后任何搭载这两个芯片的开发板,只要适配了RT-Thread,就能被轻松驱动。代码被更多人用到,也是一件很有成就感的事。
关键代码
接下来分几个关键节点,讲一下具体实现。
点亮LCD
首先是点亮LCD,我们需要先配好底层的SPI接口,但是RT-Thread在RT1021的官方代码中,没有启用SPI的配置,我们需要先编辑“board/Kconfig”,添加如下代码,这样再次进入menuconfig就能看到相关选项了。
menuconfig BSP_USING_SPI
bool "Enable SPI"
select RT_USING_SPI
select RT_USING_PIN
default n
if BSP_USING_SPI
config BSP_USING_SPI4
bool "Enable SPI4"
default n
config BSP_SPI4_USING_DMA
bool "Enable SPI4 DMA xfer"
depends on BSP_USING_SPI4
select BSP_USING_DMA
default n
config BSP_SPI4_RX_DMA_CHANNEL
depends on BSP_SPI4_USING_DMA
int "Set SPI4 RX DMA channel (0-32)"
default 4
config BSP_SPI4_TX_DMA_CHANNEL
depends on BSP_SPI4_USING_DMA
int "Set SPI4 TX DMA channel (0-32)"
default 5
endif
这次的LCD主控是ST7735,RT-Thread官方组件包里不存在,但网上有很多相关代码,正巧找到了一个大佬实现的驱动,将它引入到工程中,在“board/Kconfig”中添加并启用如下配置项,就可以驱动屏幕。
menu "Onboard Peripheral Drivers"
config BSP_USING_LCD_ST7735
bool "Enable LCD ST7735"
select BSP_USING_SPI
default n
LVGL的移植
话说现在LVGL已经基本成了单片机屏幕的软件标配,我们这次就从0开始为这块新板卡移植LVGL。
移植LVGL分为两部分,一个是屏幕填充函数,另一个是输入设备。
屏幕填充函数我最开始是使用的打点函数,屏幕会有肉眼可见的拉窗帘效果,帧率非常低。随后换成区域填充后,速度就起飞了。
static void tft_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
rt_sem_take(s_frameSema, RT_WAITING_FOREVER);
// int32_t x,y;
// for(y = area->y1; y <= area->y2; y++) {
// for(x = area->x1; x <= area->x2; x++) {
// // 画点函数
// lcd_draw_pixel(x,y, color_p->full );
// color_p++;
// }
// }
DCACHE_CleanInvalidateByRange((uint32_t)color_p, DISP_BUF_SIZE);
uint16_t width = area->x2 - area->x1 + 1;
uint16_t height = area->y2 - area->y1 + 1;
// 区域填充函数
lcd_fill_area(area->x1, area->x2, area->y1, area->y2, width*height*2, (void*)color_p);
rt_sem_release(s_frameSema);
lv_disp_flush_ready(disp_drv);
}
因为RT-Thread组件库支持XPT2046触摸芯片,触摸屏的移植很容易,写好下面的适配函数就ok了。
#include "drv_xpt2046.h"
rt_device_t touch = RT_NULL;
static struct rt_touch_data read_data;
/*Initialize your touchpad*/
static void touchpad_init(void)
{
/*Your code comes here*/
//Find the touch device
touch = rt_device_find("xpt0");
if (touch == RT_NULL)
{
rt_kprintf("can't find device:%s\n", "xpt0");
while (1);
}
if (rt_device_open(touch, RT_DEVICE_FLAG_INT_RX) != RT_EOK)
{
rt_kprintf("open device failed!");
while (1);
}
}
/*Will be called by the library to read the touchpad*/
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
/*Save the pressed coordinates and the state*/
if(touchpad_is_pressed()) {
touchpad_get_xy(&last_x, &last_y);
data->state = LV_INDEV_STATE_PR;
}
else {
data->state = LV_INDEV_STATE_REL;
}
/*Set the last pressed coordinates*/
data->point.x = last_x;
data->point.y = last_y;
}
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
/*Your code comes here*/
rt_memset(&read_data, 0, sizeof(struct rt_touch_data));
return rt_device_read(touch, 0, &read_data, 1) == 1 ? true : false;
}
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)
{
/*Your code comes here*/
(*x) = read_data.x_coordinate;
(*y) = LV_VER_RES_MAX - read_data.y_coordinate;
}
驱动光强传感器
相比于前面的驱动,光强传感器我踩到了比较多的坑:
一方面RT1021的硬件I2C怎么也驱动不起来,引脚直接没有波形输出。迫不得已我启用了RT-Thread组件包中的模拟I2C,这需要配两个引脚就好了,I2C引脚总算有了波形输出。
另一方面,板载的BH1730传感器,RT-Thread组件包没有相应的驱动,只有一个BH1750驱动,本以为名字这么像,应该能来个向下兼容。但是翻代码和手册后发现两个传感器内部区别很大,完全不通用。这下只好自己实现一个BH1730驱动了。
好在BH1730传感器的交互很简单,只需要先写好寄存器读写的操作,然后使用基本函数读写相应的初始化和数据等寄存器就可以了。
在进行协议debug的时候,我非常建议配合逻辑分析仪来进行调试,我最开始在读BH1730寄存器的函数里,因为复制粘贴的原因,错误地把第一个RT_I2C_WR写成了RT_I2C_RD,导致怎么也读不出数据。无奈之下想到了它刚刚买的十二指神探,把它刷成了逻辑分析仪固件。
果然一看波形立马发现了问题。
经过一番配置,终于拿到了想要的波形。下面是逻辑分析仪捕捉的,读取ID寄存器的正确交互。
输出PWM
RT1021的FlexPWM外设功能很强大,导致配置起来也相当复杂,好在RT-Thread已经适配好了PWM驱动,屏幕背光引脚对应的是PWM2的通道2,并且输出是B引脚,因此要对drv_pwm.c的驱动代码做一点小修改。
//Pwm_Signal.pwmChannel = DEFAULT_COMPLEMENTARY_PAIR;
Pwm_Signal.pwmChannel = kPWM_PwmB; // 改成通道B
Pwm_Signal.level = DEFAULT_POLARITY;
Pwm_Signal.dutyCyclePercent = duty;
PWM_SetupPwm(base, pwm_submodule, &Pwm_Signal, 1, kPWM_CenterAligned, fre, PWM_SRC_CLK_FREQ);
PWM_SetPwmLdok(base, pwm_submodule, true);
经过这些改动,PWM也可以输出波形了。
亮度Gamma矫正
至此,所有的外设都成功驱动起来了,但题目中有个要求“依照人眼对亮度的感知曲线,自动调整屏幕亮度”。我们还需要做最后一步,建立光强与屏幕背光PWM占空比的映射函数。这里我想到了Gamma矫正:
Gamma 矫正是图像学中的一个概念,它的主要作用是调整图像的对比度和亮度,使其更符合人眼的视觉感知。
人眼的感知亮度与实际亮度并非成线性关系,而是遵循幂律关系。Gamma 矫正函数可以用来模拟这种非线性关系,使图像的亮度更加接近人眼的感知亮度。这里我们使用它来建立光强与控制占空比的映射关系。
代码实现如下:
// Gamma校正参数
#define GAMMA 2.2
// 将0到65535范围内的光强值映射到0到1范围内的PWM占空比
float mapIntensityToPWM(uint32_t intensity) {
// 将光强值映射到0到1范围内
float normalizedIntensity = intensity / 65535.0;
// Gamma校正
float correctedIntensity = pow(normalizedIntensity, 1.0 / GAMMA);
return correctedIntensity;
}
void auto_backlight(void *parameter)
{
rt_device_t dev;
struct rt_sensor_data sensor_data;
rt_uint32_t period, pulse;
float percent = 0.01f;
period = 1000000; /* 1s,这里单位是纳秒ns,1ms等于10的6次方纳秒ns*/
pulse = 200000; /* PWM脉冲宽度值,单位为纳秒ns */
/* 查找PWM设备 */
pwm_dev = (struct rt_device_pwm *)rt_device_find(PWM_DEV_NAME);
if (pwm_dev == RT_NULL)
{
rt_kprintf("pwm sample run failed! can't find %s device!\n", PWM_DEV_NAME);
}
rt_kprintf("pwm sample run ! find %s device!\n", PWM_DEV_NAME);
/* 设置PWM周期和脉冲宽度 */
rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL2, period, pulse);
/* 使能设备 */
rt_pwm_enable(pwm_dev, PWM_DEV_CHANNEL2);
/* 查找传感器设备 */
dev = rt_device_find(SAMPLE_SENSOR_NAME);
/* 以只读及轮询模式打开传感器设备 */
rt_device_open(dev, RT_DEVICE_FLAG_RDONLY);
while(1) {
if (rt_device_read(dev, 0, &sensor_data, 1) == 1)
{
LOG_I("light:%5d lux", sensor_data.data.light);
percent = mapIntensityToPWM(sensor_data.data.light);
pulse = period * percent;
rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL2, period, pulse);
rt_thread_mdelay(100);
} else {
break;
}
}
rt_pwm_disable(pwm_dev, PWM_DEV_CHANNEL2);
rt_device_close(dev);
}
效果展示
活动感想
在这次活动中,学到了RT1021 PWM、LVGL移植、逻辑分析仪辅助调试、RT-Thread Sensor设备、Gamma矫正等非常多的东西,可以说是收获颇丰。在交流群里也认识了很多志同道合的小伙伴,很期待能够在接下来地活动中继续和大家见面。
最后,感谢硬禾学堂的寒假在家一起练活动,祝硬禾的活动越办越好!