本文旨在记录我在裸机开发K210时的一些经历和遇到的问题。
用canMV IDE开发K210时,我发现官方API提供的图像处理不能完全满足我的需求,但用micropython的for循环来实现计算在性能上显然是不现实的。一拍脑门,我就打算试试用C来开发。
主要资料
来自GitHub用户Kendryte · GitHub发布的:
SDK源码:https://github.com/kendryte/kendryte-freertos-sdk
嘉楠官方例程:https://github.com/kendryte/kendryte-standalone-demo
canmv源码:https://github.com/kendryte/canmv(也可在野火K210网盘资料中找到)
野火K210网盘资料:https://pan.baidu.com/s/17poZ0Vq_4zEq-n3oljPGaQ?pwd=txcd,其中能找到一份Standalone SDK的API手册。
K210 standalone C开发_k210 c语言sdk-优快云博客
环境搭建
关于环境,我参考了 SDK源码:https://github.com/kendryte/kendryte-freertos-sdk
根据README中的教程我完成了环境的搭建,在此不多赘述。
点亮LCD屏幕
我买到的开发板,使用的是st7789,很遗憾在官方例程中使用的是nt35310,我只能在canmv源码中寻找我需要的驱动
我在cammv的源码中注意到了这些文件:st7789.c、st7789.h、lcd.h、mcu_lcd.c、rgb_lcd.c。为了满足它们的依赖,我还找到了:sipeed_spi.h、sipeed_type.h、sipeed_spi.c(这三者是cammv对底层spi.h的封装)、global_config.h(这个很明显是全局配置文件,有坑的是,这个文件初始是不存在于项目中的,只有当项目构建时才会动态生成,为此我在project文件夹中随意找了个项目,并用cmake工具尝试构建了一些,当然没有成功,但是成功生成了global_config.h文件)。
然后我对部分文件进行了一些修改,再在自己的代码中封装了一个lcd_init()。
#include <fpioa.h>
#include <bsp.h>
#include <dmac.h>
#include "lcd.h"
#define CAM_WIDTH_PIXEL 320
#define CAM_HIGHT_PIXEL 240
#define DISPLAY_BUFFER_SIZE (CAM_WIDTH_PIXEL * CAM_HIGHT_PIXEL * 2)
lcd_t *lcd = &lcd_mcu;
void lcd_init()
{
lcd_para_t *lcd_para = lcd->lcd_para;
lcd_para->oct = true;
lcd_para->freq = 20000000;
lcd_para->rst_pin = 29;
lcd_para->cs_pin = 28;
lcd_para->dcx_pin = 27;
lcd_para->clk_pin = 26;
sysctl_set_spi0_dvp_data(1);
fpioa_set_function(27, FUNC_GPIOHS0 + DCX_GPIONUM);
fpioa_set_function(28, FUNC_SPI0_SS3);
fpioa_set_function(26, FUNC_SPI0_SCLK);
fpioa_set_function(29, FUNC_GPIOHS0 + RST_GPIONUM);
fpioa_set_function(30, FUNC_GPIOHS0 + RD_GPIONUM);
gpiohs_set_drive_mode(RD_GPIONUM, GPIO_DM_OUTPUT);
gpiohs_set_pin(RD_GPIONUM, GPIO_PV_HIGH);
lcd->init(lcd_para);
lcd->set_direction(DIR_YX_LRDU);
lcd->clear(RED);
}
注意调用fpioa_set_function时要确认自己板子的引脚,必要的话可以向客服请求原理图。
在编译时,如果没有对lcd.c进行修改,可能会报出maixpy_sdcard_loading和dual_func未定义。对前者,我选择把相关代码全部删除,因为我暂时还没有写关于SD卡的代码,spi不会冲突。对后者,可以在main文件中添加以下代码(该代码可以参考canmv源码中的maixpy_main.c):
typedef int (*dual_func_t)(int);
volatile dual_func_t dual_func;
int core1_function(void *ctx)
{
uint64_t core = current_coreid();
while (1)
{
if (dual_func)
{
dual_func(core);
dual_func = NULL;
}
else
{
msleep(10);
}
}
}
然后记得在主函数中要调用
fpioa_init();
dmac_init();
register_core1(core1_function, NULL);
这样就可以得到一个纯色的lcd屏啦。
摄像头接收
我买到的摄像头是ov2640,很幸运,该驱动在官方例程中就能找到。当然在canmv源码中也能找到相关驱动,但它的依赖关系就复杂了,因此我不推荐使用。
参照例程代码、官方手册示例(在野火的资料里找到的)和博主,我完成了自己的代码。
首先是回调函数:
void on_irq_dvp(void*ctx)
{
if(dvp_get_interrupt(DVP_STS_FRAME_FINISH))
{
g_dvp_buff = (g_dvp_buff == g_lcd_gram0) ? g_lcd_gram1 : g_lcd_gram0;
dvp_set_display_addr((uint32_t)g_dvp_buff);
dvp_clear_interrupt(DVP_STS_FRAME_FINISH);
g_dvp_finish_flag = true;
}
else
{
if(!g_dvp_finish_flag)
dvp_start_convert();
dvp_clear_interrupt(DVP_STS_FRAME_START);
}
return 0;
}
然后是初始化(相关引脚的重映射没有写在该函数中):
#include <dvp.h>
#include <plic.h>
#include <iomem.h>
#include "ov2640.h"
extern uint16_t *g_lcd_display_buff;
volatile bool g_dvp_finish_flag = false;
uint16_t *g_lcd_gram0;
uint16_t *g_lcd_gram1;
void dvp_cam_init(void)
{
sysctl_set_spi0_dvp_data(1);
/* DVP初始化,设置sccb的寄存器长度为8bit */
dvp_init(8);
/* 设置输入时钟为24000000*/
dvp_set_xclk_rate(24000000);
/* 使能突发传输模式 */
dvp_enable_burst();
/* 关闭AI输出模式,使能显示模式 */
dvp_set_output_enable(DVP_OUTPUT_AI, 0);
dvp_set_output_enable(DVP_OUTPUT_DISPLAY, 1);
/* 设置输出格式为RGB */
dvp_set_image_format(DVP_CFG_RGB_FORMAT);
/* 设置输出像素大小为320*240 */
dvp_set_image_size(CAM_WIDTH_PIXEL, CAM_HIGHT_PIXEL);
/* 设置DVP的显示地址参数和中断 */
g_lcd_gram0 = (uint16_t *)iomem_malloc(DISPLAY_BUFFER_SIZE);
g_lcd_gram1 = (uint16_t *)iomem_malloc(DISPLAY_BUFFER_SIZE);
g_dvp_buff = g_lcd_gram0;
dvp_set_display_addr((uint32_t)g_dvp_buff);
dvp_config_interrupt(DVP_CFG_START_INT_ENABLE | DVP_CFG_FINISH_INT_ENABLE, 0);
dvp_disable_auto();
plic_set_priority(IRQN_DVP_INTERRUPT, 1);
plic_irq_register(IRQN_DVP_INTERRUPT, on_irq_dvp, NULL);
plic_irq_enable(IRQN_DVP_INTERRUPT);
ov2640_init();
dvp_clear_interrupt(DVP_STS_FRAME_START | DVP_STS_FRAME_FINISH);
dvp_config_interrupt(DVP_CFG_START_INT_ENABLE | DVP_CFG_FINISH_INT_ENABLE, 1);
}
在此,我要提一嘴我遇到的一个坑了,就是volatile这个关键字。在最初我没有给g_dvp_finish_flag变量加上volatile关键字,结果发现每次按下reset按钮后,屏幕就会显示一张图片,然后卡死。经调试,发现是由于缓存一致性冲突引起的,K210的cpu核心有一级缓存,中断函数只改变了内存中的变量,而cpu核心在先前已经从内存读取了这个变量,因此cpu默认从缓存中读取,而缓存中的变量并没有从内存更新。
摄像头显示
照理来说,既然lcd驱动完成了,摄像头读取也完成了,那么显示应当是很简单的事情了,只要在主循环中调用函数就行了。但是我对canmv源码中的图片显示函数并不满意,因为直接复制下来的代码,是会把传输得到的图像数据复制一份再发送到lcd屏幕中。我认为,这复制一份的行为,是完全没有必要的,因此首先要对lcd驱动进行一番修改。
在lcd_mcu.c中:
volatile bool g_lcd_draw_picture_finish_flag = true;
static uint32_t g_pixs_draw_pic_size = 0;
static uint32_t g_pixs_draw_pic_half_size = 0;
static uint16_t local_lcd_buff[320 * 480];
static int swap_pixs_half(int core)
{
uint32_t i;
uint16_t *p = g_pixs_draw_pic;
for(i = g_pixs_draw_pic_half_size; i < g_pixs_draw_pic_size; i += 2)
{
local_lcd_buff[i] = *(p + 1);
local_lcd_buff[i + 1] = *p;
p += 2;
}
return 0;
}
static void mcu_lcd_draw_picture(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint8_t *ptr)
{
g_lcd_draw_picture_finish_flag = false;
g_lcd_display_buff = (uint16_t *)ptr;
lcd_set_area(x1, y1, x1 + width - 1, y1 + height - 1);
g_pixs_draw_pic_size = width * height;
if(g_pixs_draw_pic_size % 2)
{
tft_write_half(g_lcd_display_buff, g_pixs_draw_pic_size);
}
else
{
{
g_pixs_draw_pic_half_size = g_pixs_draw_pic_size / 2;
g_pixs_draw_pic_half_size = (g_pixs_draw_pic_half_size % 2) ? (g_pixs_draw_pic_half_size + 1) : g_pixs_draw_pic_half_size;
g_pixs_draw_pic = p + g_pixs_draw_pic_half_size;
dual_func = swap_pixs_half;
for(i = 0; i < g_pixs_draw_pic_half_size; i += 2)
{
local_lcd_buff[i] = *(p + 1);
local_lcd_buff[i + 1] = *p;
p += 2;
}
while(dual_func)
;
}
tft_write_word((uint32_t *)local_lcd_buff, g_pixs_draw_pic_size / 2);
}
g_lcd_draw_picture_finish_flag = true;
}
然后在main.c中定义以下引脚重映射函数和电压域分配函数:
static void io_mux_init(void)
{
/* Init DVP IO map and function settings */
fpioa_set_function(18, FUNC_CMOS_VSYNC);
fpioa_set_function(19, FUNC_CMOS_RST);
fpioa_set_function(20, FUNC_CMOS_HREF);
fpioa_set_function(21, FUNC_CMOS_PWDN);
fpioa_set_function(22, FUNC_CMOS_XCLK);
fpioa_set_function(23, FUNC_CMOS_PCLK);
fpioa_set_function(25, FUNC_SCCB_SCLK);
fpioa_set_function(24, FUNC_SCCB_SDA);
/* Init SPI IO map and function settings */
fpioa_set_function(27, FUNC_GPIOHS0 + DCX_GPIONUM);
fpioa_set_function(28, FUNC_SPI0_SS3);
fpioa_set_function(26, FUNC_SPI0_SCLK);
fpioa_set_function(29, FUNC_GPIOHS0 + RST_GPIONUM);
sysctl_set_spi0_dvp_data(1);
}
static void io_set_power(void)
{
sysctl_set_power_mode(SYSCTL_POWER_BANK3, SYSCTL_POWER_V18);
sysctl_set_power_mode(SYSCTL_POWER_BANK4, SYSCTL_POWER_V18);
}
最后是main()函数:
int main(void)
{
sysctl_pll_set_freq(SYSCTL_PLL0, 800000000UL);
sysctl_pll_set_freq(SYSCTL_PLL1, 160000000UL);
sysctl_pll_set_freq(SYSCTL_PLL2, 45158400UL);
fpioa_init();
dmac_init();
plic_init();
sysctl_enable_irq();
io_mux_init();
io_set_power();
register_core1(core1_function, NULL);
dvp_cam_init();
lcd_init();
while(1)
{
while(g_dvp_finish_flag == 0)
;
g_dvp_finish_flag = 0;
lcd->draw_picture(0, 0, 320, 240, (uint8_t*)((g_dvp_buff == g_lcd_gram0) ? g_lcd_gram1 : g_lcd_gram0));
}
}
这下就大功告成啦。
接下来我就可以对读到的摄像头数据做任何我想做的事情了。
使用Arduino开发K210
在完成了以上工作之后,我突然在嘉楠官网发现了这个:开始 — Arduino-K210 文档 (canaan-creative.com)
于是我去尝试了一下,下载下来后,我翻看了一下源码,在驱动中,我发现了大量注释调的函数,似乎是还在施工中。
然后我悲伤地发现他没有我买到的版型,但我在board_config.md中找到了添加版型的教程,然后按照从客服那里要到的原理图我,我添加了我的版型。
接着我用官方的例程进行了测试比如test_rotation.ino、screen_display.ino,发现lcd屏可以正常打开,但摄像头驱动有问题,原因暂时未知,还有待排查。过程中我还下载了一个依赖库Adafruit-GFX,移出了一些不兼容的库。