uboot实现JPG LOGO显示
以下基于AKNYKA37E平台
建议阅读文章前可以先了解uboot启动流程uboot启动流程分析
无论是哪种启动介质,基本流程都相似,根据下面的uboot启动流程图,结合代码进行理解
1、board_init_f——板级前置初始化
根据上述链接uboot启动流程,跟随链接中uboot启动流程图,分析一下
board_init_f
这个函数,该函数位于arch/arm/lib/board.c
#ifdef CONFIG_LCD
/*
* no define macro define CONFIG_FB_ADDR
*/
#ifdef CONFIG_FB_ADDR
gd->fb_base = CONFIG_FB_ADDR;
#else
/*
* reserve memory for LCD display (always full pages)
*/
// printf("%s ======================>>%d\n",__func__,__LINE__);
addr = lcd_setmem(addr);
gd->fb_base = addr;
#endif /* CONFIG_FB_ADDR */
#endif /* CONFIG_LCD */
board_init_f
函数,使用配置fb首地址CONFIG_FB_ADDR
或者调用lcd_setmem
获取fb大小,这里面有板级相关函数需要实现,不过为了先能启动uboot
,没有打开fb选项。addr
值就是fb首地址,gd->fb_base
保存fb首地址。
但奇怪的事,笔者将下面两句代码屏蔽掉并不影响lcd的使用,依旧可以显示logo,感觉分布分配都不会影响LCD的显示,望有大神路过,请求评论解惑
gd->relocaddr = lcd_setmem(gd->relocaddr);
gd->fb_base = gd->relocaddr;
继续进入lcd_setmem
函数,这个函数用来分配帧缓存地址, 其定义在common/lcd.c
/************************************************************************/
/* ** ROM capable initialization part - needed to reserve FB memory */
/************************************************************************/
/*
* This is called early in the system initialization to grab memory
* for the LCD controller.
* Returns new address for monitor, after reserving LCD buffer memory
*
* Note that this is running from ROM, so no write access to global data.
*/
ulong lcd_setmem(ulong addr)
{
ulong size;
int line_length;
/*
* accord config for lcd fb base addr request reserved space
*/
debug("LCD panel info: %d x %d, %d bit/pix\n", panel_info.vl_col,
panel_info.vl_row, panel_info.vl_bpix == LCD_COLOR24 ? 24 : NBITS(panel_info.vl_bpix));
size = lcd_get_size(&line_length);
/* Round up to nearest full page, or MMU section if defined */
size = ALIGN(size, CONFIG_LCD_ALIGNMENT);
addr = ALIGN(addr - CONFIG_LCD_ALIGNMENT + 1, CONFIG_LCD_ALIGNMENT);
/* Allocate pages for the frame buffer. */
addr -= size;
debug("Reserving %ldk for LCD Framebuffer at: %08lx\n", size >> 10, addr);
printf("uboot display lcd logo!\n");
printf("\033[35m uboot time %s \033[0m\n", __TIMESTAMP__);
return addr;
}
panel_info
变量 ak_fb.c
驱动中定义,定义了具体 LCD 的相关信息,lcd_get_size
函数来获得帧缓存大小。
/*
* board_init_f(arch/arm/lib/board.c) calls lcd_setmem() which needs
* panel_info.vl_col, panel_info.vl_row and panel_info.vl_bpix to reserve
* FB memory at a very early stage, i.e even before exynos_fimd_parse_dt()
* is called. So, we are forced to statically assign it.
* cdh:check ok , 10 inch lcd(1280x800), 24bit
*/
vidinfo_t panel_info = {
.vl_col = LCD_XRES,
.vl_row = LCD_YRES,
.vl_width = LCD_XRES,
.vl_height = LCD_YRES,
.vl_bpix = LCD_COLOR24,
};
之所以调用lcd_setmem
分配FB内存,参考board_init_f介绍所述,uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”。因此在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小。
这么做的目的:
- 给 Linux 腾出空间,防止 Linuxkernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。
- uboot是在某些只读存储器上运行,比如ROM、nor flash等等。需要将这部分代码拷贝到DDR上才能完整运行uboot。
到这board_init_f
初始化中关于LCD的相关操作就结束了,跟进uboot启动流程图,继续定位到board_init_r
这个函数中
2、board_init_r——板级后置初始化
经过Uboot重定向后,Uboot运行于新的地址空间,
board_init_r
主要作为Uboot运行的最后初始化步骤。该函数位于arch/arm/lib/board.c
函数与board_init_f
类似,主要用于初始化各类外设信息,包括:initr_dm
DM模型初始化,initr_mmc
MMC驱动初始化等,且对gd , bd 数据结构赋值初始化,最终,uboot就运行到了run_main_loop
,进而执行main_loop
这个函数。
跟着board_init_r
进入stdio_init
函数,该函数对多个多个stdio设备进行初始化,其中包括LCD
stdio_init(); /* get the devices list going. */
int stdio_init (void)
{
#if defined(CONFIG_NEEDS_MANUAL_RELOC)
/* already relocated for current ARM implementation */
ulong relocation_offset = gd->reloc_off;
int i;
/* relocate device name pointers */
for (i = 0; i < (sizeof (stdio_names) / sizeof (char *)); ++i) {
stdio_names[i] = (char *) (((ulong) stdio_names[i]) +
relocation_offset);
}
#endif /* CONFIG_NEEDS_MANUAL_RELOC */
/* Initialize the list */
INIT_LIST_HEAD(&(devs.list));
#ifdef CONFIG_SYS_I2C
i2c_init_all();
#else
#if defined(CONFIG_HARD_I2C)
i2c_init (CONFIG_SYS_I2C_SPEED, CONFIG_SYS_I2C_SLAVE);
#endif
#endif
#ifdef CONFIG_LCD
drv_lcd_init ();
#endif
#if defined(CONFIG_VIDEO) || defined(CONFIG_CFB_CONSOLE)
drv_video_init ();
#endif
#ifdef CONFIG_KEYBOARD
drv_keyboard_init ();
#endif
#ifdef CONFIG_LOGBUFFER
drv_logbuff_init ();
#endif
drv_system_init ();
serial_stdio_init ();
#ifdef CONFIG_USB_TTY
drv_usbtty_init ();
#endif
#ifdef CONFIG_NETCONSOLE
drv_nc_init ();
#endif
#ifdef CONFIG_JTAG_CONSOLE
drv_jtag_console_init ();
#endif
#ifdef CONFIG_CBMEM_CONSOLE
cbmemc_init();
#endif
return (0);
}
继续来到drv_lcd_init
函数,这里是本章的重点内容
3、drv_lcd_init——LCD设备初始化
#define JPG_LOGO_DISPLAY
int drv_lcd_init(void)
{
struct stdio_dev lcddev;
gd->fb_base = getenv_ulong("lcdloadaddr", 16, 0x83d02000);
if(gd->fb_base == 0){
printf("eror:lcd init gd->fb_base == 0\n");
return -1;
}
//debug("gd->fb_base:0x%x\n", gd->fb_base);
#ifndef JPG_LOGO_DISPLAY
if(read_logo_data(gd->fb_base) == -1)
{
printf("eror:read_logo_data fail\n");
return -1;
}
//debug("%s,line:%d, logo data:0x%x\n", __func__, __LINE__, REG32(0x83d02000+0x9000));
#else
int logo_size = 100 * 1024;
unsigned char *logo_data = malloc(logo_size);
if (read_logo_data((void*)logo_data) == -1)
{
printf("eror:read_logo_data fail\n");
return -1;
}
uboot_logo_sjpg_decode(logo_data,logo_size,gd->fb_base);
free(logo_data);
#endif
lcd_base = (void *)gd->fb_base;
lcd_init(lcd_base); /* LCD initialization */
//debug("cdh:%s, @@line:%d\n", __func__, __LINE__);
/* Device initialization */
memset(&lcddev, 0, sizeof(lcddev));
strcpy(lcddev.name, "lcd");
lcddev.ext = 0; /* No extensions */
lcddev.flags = DEV_FLAGS_OUTPUT; /* Output only */
lcddev.putc = lcd_putc; /* 'putc' function */
lcddev.puts = lcd_puts; /* 'puts' function */
///rc = stdio_register(&lcddev);
return 0;
}
此处笔者有疑问,第6行
gd->fb_base
根据getenv_ulong
函数中环境变量中取地址作LCD加载地址,那么前面lcd_setmem
分配的内存又有什么意义,uboot重定位后fb的首地址与getenv_ulong
不会造成冲突吗?根据getenv_ulong
获取的LCD加载地址,地址里的内存又是从何而来,什么时候分配的?
第12行中,JPG_LOGO_DISPLAY
用于区分是否使用JPG
图片代替RGB888
显示LOGO
,若未定义JPG_LOGO_DISPLAY
,则根据函数read_logo_data
从flash读取出RGB888数据加载到帧缓冲区中。
若烧入flash
中的数据为JPG
,则申请一块内存存放JPG
数据,再将数据从flash
读出,经过uboot_logo_sjpg_decode
函数进行解码成RGB888,丢入帧缓冲区中,JPG
与RGB888
格式LOGO
显示的区别就在于此,多了一步解码步骤,后续操作与RGB888一致。
进入uboot_logo_sjpg_decode
函数,该函数主要调用TJpgDec
—轻量级JPEG
解码器接口实现,需要用户自己实现,可单独分出一个文件夹存放关于解码JPG的所有源文件,此函数笔者存放在名为tjpg/sjpeg_decode.c
自定义文件中,与TJpgDec源码放在一起,uboot可编译链接库后调用uboot_logo_sjpg_decode
函数进行解码
int uboot_logo_sjpg_decode(unsigned char *src, int len, unsigned char *lcd_base)
{
io_source_t io_source;
io_source.raw_sjpg_data = src;
io_source.raw_sjpg_data_size = len;
io_source.raw_sjpg_data_next_read_pos = 0;
io_source.img_cache_buff = lcd_base;
uint8_t *wokb_temp = malloc(TJPGD_WORKBUFF_SIZE);
JDEC jd;
JRESULT rc = jd_prepare(&jd, input_func, wokb_temp, (size_t)TJPGD_WORKBUFF_SIZE, &io_source);
if (rc == JDR_OK)
{
/* 准备好解码。图像信息有效 */
printf("jpg:w=%d,h=%d \n", jd.width, jd.height);
rc = jd_decomp(&jd, img_data_cb, 0);
if (rc == JDR_OK)
{
/* 解码成功。你在这里已经解码图像到帧缓冲区 */
printf("logo jpg decode succeed\n");
}
else
{
printf("logo jpg decode failed \n");
}
}
else
{
printf("logo jpg prepare failed \n");
}
/* 释放工作区域 */
free(wokb_temp);
return -1;
}
继续下面步骤之前建议先访问此链接TJpgDec—轻量级JPEG解码器了解关于解码器的更多信息
在TJpgDec解码JPG前需要优先配置好解码库源码中tjpgd.h
文件中几个宏定义
#define JD_SZBUF 512 /* Size of stream input buffer */
#define JD_FORMAT 0 /* Output pixel format 0:RGB888 (3 BYTE/pix), 1:RGB565 (1 WORD/pix) */
#define JD_USE_SCALE 1 /* Use descaling feature for output */
#define JD_TBLCLIP 1 /* Use table for saturation (might be a bit faster but increases 1K bytes of code size) */
#define JD_SCALE 0 /* 0:1/1 1:1/2 2:1/4 3:1/8 */
配置好宏定义后,结合上述解码器链接,定义io_source_t
结构体,将此结构体作为device
参数,具体作用一会就知道了
typedef struct
{
uint8_t *raw_sjpg_data; // Used when type==SJPEG_IO_SOURCE_C_ARRAY.
uint32_t raw_sjpg_data_size; // Num bytes pointed to by raw_sjpg_data.
uint32_t raw_sjpg_data_next_read_pos; // Used for all types.
uint8_t *img_cache_buff;
} io_source_t;
.raw_sjpg_data
:存储需要解码器解码的数据
.raw_sjpg_data_size
:待解码数据大小
.raw_sjpg_data_next_read_pos
:已读取的JPG数据长度
.img_cache_buff
:lcd帧缓冲地址
分配一份TJPGD_WORKBUFF_SIZE
大小的内存(这里是4096)由wokb_temp
指针接收用来作为工作缓冲区丢入jd_prepare
函数
input_func
函数作为jd_prepare
函数的第二个函数指针,用于读取输入JPG流的数据的输入函数,实现如下
/*
*@jd:指定解码会话的解码对象
*@buff:指定读缓冲器去保存读取数据。传入NULL将数据从输入流移除
*@ndata:指定从输入流读取或移除的字节数
*/
static size_t input_func(JDEC *jd, uint8_t *buff, size_t ndata)
{
/* jd_prepare函数丢入的io_source_t指针存储在JDEC *jd解码对象的device成员变量中 */
io_source_t *io = jd->device;
if (!io)
return 0;
/* JPG信息数据剩余字节大小 */
const uint32_t bytes_left = io->raw_sjpg_data_size - io->raw_sjpg_data_next_read_pos;
/* 解析JPG信息数据的字节大小 */
const uint32_t to_read = ndata <= bytes_left ? (uint32_t)ndata : bytes_left;
if (to_read == 0)
return 0;
if (buff)
{
/* JPG信息数据丢入工作缓存区 */
memcpy(buff, io->raw_sjpg_data + io->raw_sjpg_data_next_read_pos, to_read);
}
/* 刷新已读取的JPG数据长度 */
io->raw_sjpg_data_next_read_pos += to_read;
/*返回读取数据长度*/
return to_read;
}
jd_prepare
返回JDR_OK
代表函数执行成功,且编码对象是有效的,可进行接下来的解码操作
jd_prepare
函数是JPEG
解码会话的第一阶段。它分析JPEG
图像和创建解码参数表。函数成功后,会话准备好在jd_decomp
函数解码JPEG图像。应用程序可以参考JPEG
解码对象中存储的尺寸大小。这个信息将用于在后续的解码阶段配置输出设备device
和参数
jd_decomp
解码JPEG图像并输出RGB数据,参数三作为解码后缩小图像的比例,禁用此功能设为0,参数二img_data_cb
用户定义的输出函数。jd_decomp
调用这个函数去输出解码JPEG
图像的RGB
形式,写解码像素到输出设备
/*
*@jd:指定解码会话的解码对象
*@data:像素数据流。是按照tjpgdcnf.h文件中JD_FORMAT选项指定的格式组织
*@rect:指定在图像中的矩形区域去输出RGB位图
*/
static int img_data_cb(JDEC *jd, void *data, JRECT *rect)
{
io_source_t *io = jd->device;
uint8_t *cache = io->img_cache_buff;
uint8_t *buf = data;
const int INPUT_PIXEL_SIZE = 3;//RGB
/* 行区域像素 */
const int row_width = rect->right - rect->left + 1; // Row width in pixels.
/* 行区域数据大小 */
const int row_size = row_width * INPUT_PIXEL_SIZE; // Row size (bytes).
int y = 0;
for (y = rect->top; y <= rect->bottom; y++)
{
int row_offset = y * jd->width * INPUT_PIXEL_SIZE + rect->left * INPUT_PIXEL_SIZE;
/* 根据偏移地址拷贝至帧缓存区 */
memcpy(cache + row_offset, buf, row_size);
buf += row_size;
}
}
在这个函数中,位图发送到帧缓冲。第一个像素是位图矩形的左上角位置,最后一个像素是右下角位置。 jd_decomp
函数执行结束后,返回JDR_OK
代表解码完成,帧缓冲区现在已经填充好RGB数据,释放掉分配的工作缓冲区,解码器TJpgDec解码这部分就结束了
到此只需要将TJpgDec库移植进uboot目录下,链接编译后即可在flash中读取JPG数据进行解码显示在LCD上,LCD初始化部分稍后再说明,先说说移植部分,用户可进入此链接下载TJpgDec解码器源码,下载完后解压到tjpg文件夹下(解压目录可自定义,笔者以自己项目为例),下面是tjpg文件的目录结构目录结构
sjpeg_decode.c
定义了上面提到的几个函数接口uboot_logo_sjpg_decode
、img_data_cb
、input_func
,及结构体io_source_t
,解码器源文件除tjpgd.h
需要配置相关宏其余无需改动,笔者将tjpg文件夹放置在/uboot/common/
目录下,修改目录下common
目录下Makefile
添加链接后到os/
目录下运行build_uboot.sh
编译后烧录即可
COBJS-$(CONFIG_LCD) += tjpg/sjpeg_decode.o
COBJS-$(CONFIG_LCD) += tjpg/tjpgd.o
编译成功后会生成相关的xxx.o
与xxx.su
需要注意一点,
JPG
的文件属性需要与设备树中配置LCD LOGO
属性一致,否则LOGO显示会不正常
&lcdc {
pinctrl-names = "default";
pinctrl-0 = <&lcd_reset_pins &lcd_power_pins>;
reset-pins = <&gpio 34 GPIO_ACTIVE_HIGH>;
pwr-gpio = <&gpio 36 GPIO_ACTIVE_HIGH>;
backlight-gpio = <&gpio 82 GPIO_ACTIVE_HIGH>;//82
lcd-logo-width = <1024>;
lcd-logo-height = <600>;
//[fmt1:fmt0] 00 16bits input(RGB565 or BGR565)
//[fmt1:fmt0] 01 24bits input(RGB888 or BGR888)
//[fmt1:fmt0] 10 or 11 32bits input(ARGB888 ABGR888 RGBA888 BGRA888)
lcd-logo-fmt0 = <1>;
lcd-logo-fmt1 = <0>;
lcd-logo-rgb-seq = <0>; //0 for BGR, 1 for RGB
// lcd-rgb-bg-color=<0xFFFFFF>;
lcd-fb-type = <1>; //0:single buffer; 1:double buffer
status = "okay";
};
若图片显示效果类似下图,像素有略带偏移,多半是JD_FORMAT
配置不正确,如RGB888
配置成RGB565
,将JD_FORMAT
配置成正确即可
若图片显示效果类似下图,颜色出现偏差,则有可能设备树中lcd-logo-rgb-seq
属性配置错误
若图片显示还是不正常,建议将图片进行一次重新编码,这里笔者用的是工具是
ACDSee Photo Studio Professional 2022
重新进行另存为并在Options
选项中关闭Optimize Huffman codes
的勾选保存,后将jpg
后缀改成rgb
即可,注意文件大小不要超过drv_lcd_init
函数中给logo
分配的缓存大小