You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

19 KiB

实战一嵌入式开发如何使用C语言开发智能电灯

你好,我是郭朝斌。

作为RISC-V开发板动手实践的第一讲我们从智能灯的嵌入式开发开始吧。

智能灯硬件最重要的功能是控制LED灯的颜色和开关这就涉及到PWM信号的生成和继电器的控制。之前我们是使用Python语言在NodeMCU开发板上实现的。那么这些功能如何在RISC-V芯片上使用C语言实现呢

别着急,我来一步一步地讲解一下。

开发板介绍

上一讲已经提到我们使用的开发板是平头哥T-Head公司设计开发的RVB2601 开发板。主控芯片代号是CH2601它基于平头哥开源的玄铁E906处理器内核IP设计开发具体的指令集架构是RV32IMACX。

根据上节学到的RISC-V指令集知识你可以知道这代表它采用 RISC-V 32bit 基本整数指令子集,并且包含整数乘法与除法指令子集,不可中断指令(也称作存储器原子操作指令)子集,压缩指令子集,这些标准扩展指令集。

其中,压缩指令子集对于嵌入式芯片非常重要,它可用于提高代码密度,节省存储成本。最后的代号"X"表示玄铁E906自定义的一些扩展指令集这也是RISC-V指令集架构可扩展性的体现。

说了这么多我们来看一下RVB2601开发板的实物图。

作为一款嵌入式系统开发板RVB2601在用户交互体验上下了一番功夫提供了一块OLED屏幕两个MIC输入一个扬声器一个三色LED灯还有两个实体按键。基于这些器件你可以直接动手实验很多有趣的应用。

更重要的是这些外围器件都是通过跳线帽与主控芯片CH2601的I/O接口相连的这就是说我们可以把跳线帽去掉将我们自己的传感器、继电器等器件与芯片的I/O接口连接开发我们的特定的应用。

当然,在这之前,我们需要先了解一下 RVB2601 开发板的I/O接口分布及功能。我绘制了一张I/O接口功能图供你参考。官方文档中也有相关的描述,你也可以参考,但是介绍比较分散,你可能经常需要查看文档的多个位置来确认一个接口的功能。

开发环境初体验

了解完硬件的基本情况,我们来看一下软件的开发工具和流程。

第一步 安装集成开发环境

平头哥提供了一个集成开发环境名称是剑池CDK。在这个软件开发环境里图形化的操作界面更利于我们使用。

不过CDK只支持 Windows操作系统如果你使用Mac电脑的话可以像我一样使用 VirtualBox 虚拟机软件在Mac电脑上运行一个Windows虚拟机。VirtualBox是完全免费的当然其他商用的虚拟机软件也可以使用。

第二步 程序开发

安装好IDE集成开发环境我们来创建一个工程项目。为了简便、快速地帮你理解我们直接基于 IDE 提供的模版工程创建一个示例程序。

在IDE的欢迎页面中点击右上角的“新建工程”

接着,你可以在新页面中找到顶部的搜索框,然后输入"ch2601"。

如果IDE没有显示欢迎页面你可以点击工具栏右侧的平头哥Logo图标重新打开欢迎页面。

点击搜索你可以看到所有关于芯片CH2601的工程项目。

我们选择与智能灯最接近的跑马灯项目作为模板。点击“ch2601_marquee_demo”条目右侧的“创建工程”。在弹出的“新建工程”窗口中输入一个工程名称比如“Led”然后点击“确认”。

在IDE左侧我们可以看到新建工程“Led”的工程结构

  • “app”目录中的文件是工程的应用代码。
  • “.gdbinit”文件是GDB调试程序的初始化参数。
  • “SConstruct”文件是Scons软件构建工具的配置文件。
  • “sdk_chip_ch2601”节点中是YOC平台相关的基础库。

第三步 编译

现在我们新建的Led工程中是跑马灯应用的完整代码。我们先不做任何修改直接编译、运行一下。你可以点击工具栏中的编译图标启动工程编译。

或者从“Project”菜单中选择“Build Active Project”。

从这里我们也可以看出C语言和Python语言的不同C语言开发的代码在运行之前必须先进行编译生成机器指令而在这个过程中编译器可以提前发现语法错误等。

第四步 烧录

接着我们需要将编译生成的固件文件烧录到开发板中。RVB开发板是通过JTAG接口来完成固件烧录的。你需要使用USB连接线把电脑与开发板的JTAG接口相连然后点击工具栏中的烧录图标。

第五步 运行

烧录完成后你需要按一下开发板上的RST按键重置RVB2601开发板。这时就可以看到开发板上的LED灯开始三种颜色交替点亮、熄灭。

这里提醒一下如果LED灯的闪烁不正确你需要查看一下开发板上PA7、PA25和PA4的跳线帽安装位置是否正确。我收到的开发板上PA7一开始没有安装跳线帽就导致了LED灯的红色状态不工作。

智能灯的开发

接下来,我们开始智能灯的开发。

LED模块和继电器仍然使用我们之前实验中使用的器件。如果你是第一次接触这个实验或者之前的器件已经不知道跑到哪里去了也可以参考这份文档自行采购相关硬件。

至于电路连接图,你可以参考下图。

这里需要注意的是最初开发板上的J1扩展接口的15和16位、J3扩展接口的3和4位、J3扩展接口的5和6位、J3扩展接口的7和8位还有J4扩展接口的15和16位是安装着跳线帽的你在接线时需要把这些跳线帽去掉。

PWM信号生成

在本实验中LED模块的发光原理依然是PWM忘记的同学可以回去第17节复习一下。LED灯的代码如下供你参考。


/*********************
 *      INCLUDES
 *********************/
#define _DEFAULT_SOURCE /* needed for usleep() */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <aos/aos.h>
#include "app_config.h"
#include "app_init.h"
#include "csi_config.h"
#include "hw_config.h"

#include "board_config.h"
#include "drv/gpio_pin.h"
#include <drv/pin.h>
#include <drv/pwm.h>

#ifdef CONFIG_PWM_MODE

static uint32_t g_ctr = 0;
static csi_pwm_t  r;

void led_pinmux_init()
{
        //7
    csi_error_t ret;
    csi_pin_set_mux(PA7, PA7_PWM_CH7);
    csi_pin_set_mux(PA25, PA25_PWM_CH2);
    csi_pin_set_mux(PA4, PA4_PWM_CH4);

    ret = csi_pwm_init(&r, 0);
    if (ret != CSI_OK) {
            printf("===%s, %d\n", __FUNCTION__, __LINE__);
            return ;
    }
}

void rgb_light(uint32_t red, uint32_t green, uint32_t blue)
{
	csi_error_t ret;
	ret = csi_pwm_out_config(&r, 7 / 2, 300, red*300/255, PWM_POLARITY_HIGH);
    if (ret != CSI_OK) {
            printf("===%s, %d\n", __FUNCTION__, __LINE__);
            return ;
    }
    ret = csi_pwm_out_start(&r, 7 / 2);
    if (ret != CSI_OK) {
            printf("===%s, %d\n", __FUNCTION__, __LINE__);
            return ;
    }
        //25
    ret = csi_pwm_out_config(&r, 2 / 2, 300, green*300/255, PWM_POLARITY_HIGH);
    if (ret != CSI_OK) {
            printf("===%s, %d\n", __FUNCTION__, __LINE__);
            return ;
    }
    ret = csi_pwm_out_start(&r, 2 / 2);
    if (ret != CSI_OK) {
            printf("===%s, %d\n", __FUNCTION__, __LINE__);
            return ;
    }
        //4
    ret = csi_pwm_out_config(&r, 4 / 2, 300, blue*300/255, PWM_POLARITY_HIGH);
    if (ret != CSI_OK) {
            printf("===%s, %d\n", __FUNCTION__, __LINE__);
            return ;
    }
    ret = csi_pwm_out_start(&r, 4 / 2);
    if (ret != CSI_OK) {
            printf("===%s, %d\n", __FUNCTION__, __LINE__);
            return ;
    }
}
#endif 

#ifdef CONFIG_GPIO_MODE
static uint32_t g_ctr = 0;
static csi_gpio_pin_t r;
static csi_gpio_pin_t g;
static csi_gpio_pin_t b;
void led_pinmux_init()
{
    csi_pin_set_mux(PA7, PIN_FUNC_GPIO);
    csi_pin_set_mux(PA25, PIN_FUNC_GPIO);
    csi_pin_set_mux(PA4, PIN_FUNC_GPIO);
    csi_gpio_pin_init(&r, PA7);
    csi_gpio_pin_dir(&r, GPIO_DIRECTION_OUTPUT);
	csi_gpio_pin_mode(&r, GPIO_MODE_PUSH_PULL);
    csi_gpio_pin_init(&g, PA25);
    csi_gpio_pin_dir(&g, GPIO_DIRECTION_OUTPUT);
	csi_gpio_pin_mode(&g, GPIO_MODE_PUSH_PULL);
    csi_gpio_pin_init(&b, PA4);
    csi_gpio_pin_dir(&b, GPIO_DIRECTION_OUTPUT);
	csi_gpio_pin_mode(&b, GPIO_MODE_PUSH_PULL);
    g_ctr = 0;
}

//fake rgb, because of only high or low state of gpio
void rgb_light(uint32_t red, uint32_t green, uint32_t blue)
{
	(red < 50)?csi_gpio_pin_write(&r, GPIO_PIN_LOW):csi_gpio_pin_write(&r, GPIO_PIN_HIGH);
	(green < 50)?csi_gpio_pin_write(&r, GPIO_PIN_LOW):csi_gpio_pin_write(&g, GPIO_PIN_HIGH);
	(blue < 50)?csi_gpio_pin_write(&r, GPIO_PIN_LOW):csi_gpio_pin_write(&b, GPIO_PIN_HIGH);
}
#endif

在代码中,我们主要使用了 CSI 接口规范中的相关接口函数,你可以参考接口规范文档进一步了解具体细节。

比对规范文档你可能会有一个疑问接口PA7、PA25和PA4对应的PWM输出接口编号分别是CH7、CH2和CH4为什么接口函数csi_pwm_out_config和csi_pwm_out_start的channel通道号入参中填写的是编号除以2的得数取整而不是编号本身呢

这涉及到CH2601芯片对于PWM的内部具体设计。

CH2601芯片内部有6个PWM发生器对应着编号05的通道。每个PWM发生器对应2个输出接口这两个接口的PWM信号波形是完全一致的。通道编号与输出接口的具体关系如下表。

从表格中你可以直观地看到PWM输出接口编号数字需要除以2并取整才可以得到PWM的通道编号。这就是上面代码处理方式的来源。

另外在上面表格中我还列出了PWM通道对应的GPIO接口。这是为了方便你在以后的应用中选择合适的PWM接口。比如在本实验的智能灯应用中我们需要三个独立的PWM通道来控制LED模块的R、G、B接口那么我们就不能选择表格里面同一格的多个GPIO接口。显然我们选择的PA7、PA25和PA4是满足这个原则的。

同时我也使用GPIO的高低电平模式实现了LED模块的控制逻辑便于你对比两种方式的异同。

继电器的控制

接下来我们看继电器控制的部分是怎么实现的。继电器的驱动同样是通过GPIO口通过输出高低电平实现它的通断控制。代码如下。

/*********************
 *      INCLUDES
 *********************/
#define _DEFAULT_SOURCE /* needed for usleep() */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <aos/aos.h>
#include "app_config.h"
#include "app_init.h"
#include "csi_config.h"
#include "hw_config.h"

#include "board_config.h"
#include "drv/gpio_pin.h"
#include <drv/pin.h>

static csi_gpio_pin_t relay;

void relay_pinmux_init() 
{
	csi_error_t ret;
	csi_pin_set_mux(PA26, PIN_FUNC_GPIO);
	csi_gpio_pin_init(&relay, PA26);
    csi_gpio_pin_dir(&relay, GPIO_DIRECTION_OUTPUT);
}

void relay_toggle(bool on)
{
	if(on)
	{
		csi_gpio_pin_write(&relay, GPIO_PIN_HIGH);
	} 
	else 
	{
		csi_gpio_pin_write(&relay, GPIO_PIN_LOW);
	}
}

主逻辑编写

最后是程序的主逻辑。我们需要先初始化LED模块和继电器使用的I/O接口然后调用LED代码的接口点亮LED灯。在主循环中我们周期性地打开、关闭继电器实现LED灯闪烁的效果。

其中lv和oled开头的函数是显示屏相关的代码。你暂时不需要关心它们我会在后面的实验中详细讲解。


/*********************
 *      INCLUDES
 *********************/
#define _DEFAULT_SOURCE /* needed for usleep() */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <aos/aos.h>
#include "aos/cli.h"

#include "app_config.h"
#include "app_init.h"
#include "csi_config.h"
#include "hw_config.h"
#include "lvgl.h"
#include "lv_label.h"
#include "oled.h"

#include "board_config.h"
#include "drv/gpio_pin.h"
#include <drv/pin.h>
#include <drv/pwm.h>

/*********************
 *      DEFINES
 *********************/
#define TAG "app"

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void led_task(void *arg);

/**********************
 *  STATIC VARIABLES
 **********************/

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

volatile uint32_t g_debug = 0;
volatile uint32_t g_debug_v = 0;

#include "csi_core.h"
/**
 * main
 */
int main(void)
{
    board_yoc_init();

    printf("===%s, %d\n", __FUNCTION__, __LINE__);
    printf("===%s, %d\n", __FUNCTION__, __LINE__);
    aos_task_new("demo", led_task, NULL, 10 * 1024);
    return 0;
}

static void lable_test(void)
{
    lv_obj_t *p = lv_label_create(lv_scr_act(), NULL);
    lv_label_set_long_mode(p, LV_LABEL_LONG_BREAK);
    lv_label_set_align(p, LV_LABEL_ALIGN_CENTER);
    lv_obj_set_pos(p, 0, 4);
    lv_obj_set_size(p, 128, 60);
    lv_label_set_text(p, "THEAD\nMARQUEE\nDEMO");
}

static void led_task(void *arg)
{
    lv_init();
    oled_init();
    lable_test();
    led_pinmux_init();
	relay_pinmux_init();
	rgb_light(255,255,0);

    while (1)
    {
		relay_toggle(true);
        lv_task_handler();
        udelay(1000 * 1000);
        lv_tick_inc(1);
        relay_toggle(false);
		udelay(1000 * 1000);
    }
}


云上实验室

如果你暂时没有RVB2601开发板是不是就只能干看了呢我发现有一个备选方案可以让你体验一下RVB2601开发板甚至其他型号的RISC-V芯片的开发板你可以访问平头哥开发者社区提供的“云上实验室”服务。

具体怎么做呢?

首先,在云上实验室页面的筛选条件区域中,直接选择“开发板型号”为 RVB2601如图所示你可以看到可供申请的所有开发板。然后点击“申请评估”可以看到这个开发板可供选择的空闲日期。你可以根据自己的需求选择一个合适的时间区间进行申请。

当申请通过后,你会收到一条短信提醒。这时,打开云上实验室页面中“我的设备”标签,就可以看到可以使用的开发板了。

点击“进入云评估”,你会进入评估控制台,页面中有详细的使用介绍。

除了体验平台提供的几个demo程序我们也可以尝试修改一下代码来运行。比如在ch2601_gui_demo 的程序,尝试修改一下屏幕显示的文字。命令可参考图片。

我改为了下图的内容:

然后,在 Host Terminal 命令行输入 make all 编译固件。

接着,输入 make flashall 烧录固件文件到开发板中。

最终的运行情况,我们可以通过摄像头来查看。

小结

到这里我们已经熟悉了RVB2601开发板的开发环境并且完成了智能灯的硬件搭建和嵌入式软件开发。

你可以看到当面对一个新的芯片和新的开发板时我们需要根据GPIO接口的功能来选择合适的接口比如选择具备PWM功能的接口连接LED模块的R、G、B通道。

针对RVB2601开发板PWM输出接口的选择还需要注意一个点——通道Channel的选择。CH0/CH1、CH2/CH3、CH4/CH5、CH6/CH7、CH8/CH9和CH10/CH11分别对应编号05的6个PWM通道。

另外因为现在的实验使用C语言嵌入式系统应用的开发步骤与之前使用Python语言是不同的包括下图所示的步骤。

如果你暂时没有开发板可以申请体验一下云上实验室。除了可以像我上面提到的那样修改代码来运行我还推荐你来动手实践一下Linux环境下的开发工具YocTools的使用方法。

思考题

最后,我想提出一个挑战,当你完成本节的实验后,是否可以为开发板上自带的用户按键开发代码,实现按键控制继电器通断的功能呢?

欢迎你在评论区分享自己的挑战成果。基于你完成的智能灯,下一节中,我将介绍一下连接物联网平台,并实现联网控制的方法。同时,也欢迎你把今天的内容分享给你的朋友,一起来“极客”一把。