使用瑞萨ThreadX实时操作系统与多传感器/线程的消息框架

欢迎阅读关于Synergy软件平台的另一篇指南。本指南将介绍如何设置多个线程来轮询多个传感器,将这些传感器的信息发送到另一个线程进行处理,最后将处理后的信息发送到输出线程。本指南将运用本系列先前指南中的概念,但旨在提供独立设置项目演示的指导。这是关于使用DK-S124的教程,该开发板可从DigiKey获取,型号RTK7DKS124S00002BU,瑞萨电子| 开发板、套件、编程器 | DigiKey

本指南包括示例程序概述、e2 studio中的项目设置/配置流程、带注释的项目源代码,以及程序输出和结论。一如既往,下方附有实用链接。

开发板用户手册:《DK-S124快速入门指南》(renesas.com)

《S124微控制器组数据手册》(renesas.com)

《S124微控制器组用户手册》(renesas.com)

Synergy软件包用户手册:瑞萨Synergy™软件包(SSP)| Renesas

《Synergy软件平台基础教程》

程序概述

假设有一个如下图的简单程序结构。

在此程序中,两个传感器线程轮询独立的传感器,并将数据作为“传感器消息”发送到第三个线程进行处理,处理后的数据作为“输出消息”发送。此示例可轻松扩展至更多传感器线程,甚至根据需要支持不同的传感器消息类型。

为简化实现,示例中将模拟传感器数据,处理过程非常轻量,输出通过UART完成。将使用的Synergy软件平台关键特性是支持多线程的瑞萨消息框架。

e2 Studio 的设置 / 配置

打开e2 studio并创建一个新的Synergy C项目。本指南将项目命名为MultiThreadMessageGuide。确保选择正确的设备(DK-S124使用R7FS124773A01CFM),并选择BSP作为项目模板。若对这些步骤不熟悉,请参阅《在DK-S124上配置和使用UART接口》指南,了解完成e2 studio初始设置后如何创建项目。

新项目打开后,进入项目配置文件的线程选项卡。由于本程序有4个线程,必须创建4个线程。依次创建“sensor_thread1”、“sensor_thread2”、“computation_thread”和“output_thread”。接下来,将添加所需的线程栈。由于我们有4个线程,每个线程通常需要占用1024字节的RAM,而在此配置下DK-S124仅有3960字节可用空间,因此需要略微缩减其中一个线程的栈大小。于是,将输出线程的"栈大小"属性从1024字节调整为800字节。

本项目仅需两个线程栈:消息框架和UART驱动。从技术上讲,消息框架可以添加到任何非HAL/通用线程中。本指南将其添加到计算线程,因为无论传感器线程如何变化,该线程始终存在。显然,UART驱动将置于输出线程,因为该线程会直接使用它。

将消息框架线程栈添加至计算线程(添加线程栈>框架>服务>消息框架)。本指南中其所有默认选项均可正常工作。接着添加UART驱动线程栈(添加线程栈>驱动>连接>UART驱动)。与先前指南不同,“用户定义的UART回调函数"将保留为"user_uart_callback”,该函数用于判断UART设备何时完成写入。与之前指南类似,接收、发送和发送结束中断优先级必须从"禁用"改为0到2之间的某个值。若对这些步骤不熟悉,请参阅《在DK-S124上配置和使用UART接口》获取添加和配置线程栈的详细说明。

现在需要通过消息标签页配置消息框架。进入消息标签页,新建名为SensorData的事件类。其余字段将自动填充。若对此步骤不熟悉,请参考《在Renesas DK-S124上使用ADC和ThreadX RTOS消息框架》的"设置消息框架"章节。

本示例中,即使输出线程也仅使用此事件类。可能该设备正在进行传感器融合,仅对外暴露一组数值。但本示例可根据需要扩展为使用不同事件类处理输出数据。

最后,需为SensorData事件类添加订阅者。由于存在多个订阅者,将使用事件实例字段进行区分。点击新建订阅者按钮,下拉选择"计算线程",并填写"起始"0和"结束"1。这意味着计算线程将接收header.event_b.class_instance值介于0到1之间的所有SensorData消息。这些消息将由传感器线程生成。接着,再添加一个订阅者。通过将“开始”和“结束”字段均填写为数字2,将输出线程订阅到所有类实例值为2的SensorData消息。计算线程将生成这些消息。注意,本可以使用单独的事件类来实现此目的,但通过这种方式可以展示如何利用类实例值来定向消息。

点击生成项目内容按钮,此时您的屏幕应显示如下:

注释源代码

首先,必须创建并填充sensordata_api.h文件。右键单击MultiThreadMessageGuide项目的“src”目录,选择新建>头文件。将名称设为sensordata_api.h。该文件将详细说明应用程序所用消息的结构。

在本示例中,假设每个传感器都有几个16位数据的通道。这些可能是来自加速度计的不同轴,或来自一系列传感器的多个温度值。每个传感器线程将发布此类型的消息。
sensordata_api.h

#ifndef _SENSORDATA_API_H_
#define _SENSORDATA_API_H_
 
#define NUMCHANNELS (uint8_t)10
 
#include "sf_message_api.h" //this is a message, so the message api is needed
 
typedef struct sensordata_payload_s
{
    sf_message_header_t header; //every message must include a header of this type
    uint8_t sensorchannel[NUMCHANNELS]; //suppose there are a number of channels from the sensor
    uint16_t sensordata[NUMCHANNELS]; //this is the data from each channel
} sensordata_payload_t; //This name is specified in "Event Class" properties as "Payload Type"
 
#endif

接下来是sensor_thread1.c和sensor_thread2.c:

sensor_thread1_entry.c

#include "sensor_thread1.h"
#include "sensordata_api.h"
 
/* Sensor Thread 1 entry function */
void sensor_thread1_entry(void)
{
    //Message init
    //sending sensordata init
    sf_message_header_t * pPostBuffer; //pointer for the buffer that must be acquired
    sf_message_acquire_cfg_t acquireCfg = {.buffer_keep =false}; //do not keep the buffer, other threads need it
    ssp_err_t errorBuff; //place for error codes from buffer acquisition to go
    sf_message_post_err_t errPost; //place for posting error codes to go
    sf_message_post_cfg_t post_cfg =
    {
      .priority = SF_MESSAGE_PRIORITY_NORMAL, //normal priority
      .p_callback = NULL //no callback needed
    };
    sensordata_payload_t * pDataPayload; //pointer to the receiving message payload
 
    while (1)
    {
        //send as message if possible
        errorBuff = g_sf_message0.p_api->bufferAcquire(g_sf_message0.p_ctrl, &pPostBuffer, &acquireCfg, 0);
        if (errorBuff==SSP_SUCCESS)
        {
            pDataPayload = (sensordata_payload_t *) pPostBuffer; //cast buffer to our payload
            pDataPayload->header.event_b.class_code = SF_MESSAGE_EVENT_CLASS_SENSORDATA; //set the event class
            pDataPayload->header.event_b.class_instance = 0; //set the class instance
            pDataPayload->header.event_b.code = SF_MESSAGE_EVENT_NEW_DATA; //set the message type
 
            for (uint8_t temp=0; temp<NUMCHANNELS;temp++) pDataPayload->sensordata[temp] = temp; //fill the payload with dummy data
            for (uint8_t temp=0;temp<NUMCHANNELS;temp++) pDataPayload->sensorchannel[temp] = temp;
            g_sf_message0.p_api->post(g_sf_message0.p_ctrl, (sf_message_header_t *) pDataPayload,
                                      &post_cfg, &errPost, TX_WAIT_FOREVER); //post the message
        }
        tx_thread_sleep (205);
    }
}

sensor_thread2_entry.c

#include "sensor_thread2.h"
#include "sensordata_api.h"
 
/* Sensor Thread 2 entry function */
void sensor_thread2_entry(void)
{
    //Message init
    //sending sensordata init
    sf_message_header_t * pPostBuffer; //pointer for the buffer that must be acquired
    sf_message_acquire_cfg_t acquireCfg = {.buffer_keep =false}; //do not keep the buffer, other threads need it
    ssp_err_t errorBuff; //place for error codes from buffer acquisition to go
    sf_message_post_err_t errPost; //place for posting error codes to go
    sf_message_post_cfg_t post_cfg =
    {
      .priority = SF_MESSAGE_PRIORITY_NORMAL, //normal priority
      .p_callback = NULL //no callback needed
    };
    sensordata_payload_t * pDataPayload; //pointer to the receiving message payload
 
    while (1)
    {
        //send as message if possible
        errorBuff = g_sf_message0.p_api->bufferAcquire(g_sf_message0.p_ctrl, &pPostBuffer, &acquireCfg, 0);
        if (errorBuff==SSP_SUCCESS)
        {
            pDataPayload = (sensordata_payload_t *) pPostBuffer; //cast buffer to our payload
            pDataPayload->header.event_b.class_code = SF_MESSAGE_EVENT_CLASS_SENSORDATA; //set the event class
            pDataPayload->header.event_b.class_instance = 1; //set the class instance
            pDataPayload->header.event_b.code = SF_MESSAGE_EVENT_NEW_DATA; //set the message type
 
            for (uint8_t temp=0; temp<NUMCHANNELS;temp++) pDataPayload->sensordata[temp] = temp+20; //fill payload with dummy data
            for (uint8_t temp=0;temp<NUMCHANNELS;temp++) pDataPayload->sensorchannel[temp] = temp+17;
            g_sf_message0.p_api->post(g_sf_message0.p_ctrl, (sf_message_header_t *) pDataPayload,
                                      &post_cfg, &errPost, TX_WAIT_FOREVER); //post the message
        }
        tx_thread_sleep (100);
    }
}

在本示例中,这两个线程几乎相同;但在实际应用中,两个传感器的轮询方式可能不同。可能一个传感器是i2c接口,另一个只是GPIO传感器,或者其中一个使用ADC读取。因此,设置两个传感器线程是合理的。本示例中,线程有两个关键区别:tx_thread_sleep时间不同以模拟不同刷新率的传感器,虚拟数据不同以模拟不同的传感器值。

接下来是计算线程。本示例中,两个传感器的值将在传递给输出线程前简单相加。在更复杂的应用中,可以对值进行过滤或组合,以从传感器读数中发现新信息。例如,当检测到有异物非常接近且温度极高时,可以读取该信息并通过UART发出紧急启动近程冷却剂的指令。以下是代码。

computation_thread_entry.c

#include "computation_thread.h"
#include "sensordata_api.h"
/* Computation Thread entry function */
void computation_thread_entry(void)
{
    //Messaging init
    ////Recieving init
    sf_message_header_t * pSensorDataHeader; //pointer to the message header
    sensordata_payload_t * pSensorDataPayload; //pointer to the message payload
 
    ////Sending init
    sf_message_header_t * pOutputBuffer; //pointer for the buffer that must be acquired
    sf_message_acquire_cfg_t acquireCfgPost = {.buffer_keep =false}; //could keep the buffer, because this thread is the only one posting to the receiving thread
    ssp_err_t errorBuffPost; //place for error codes from buffer acquisition to go
    sf_message_post_err_t errPost; //place for posting error codes to go
    sf_message_post_cfg_t post_cfg =
    {
      .priority = SF_MESSAGE_PRIORITY_NORMAL, //normal priority
      .p_callback = NULL //no callback needed
    };
    sensordata_payload_t * pOutputPost; //pointer to data to be sent to uart
 
    uint16_t lastknownsensor1values[NUMCHANNELS]; //buffer to keep sensor1 readings in
    uint16_t lastknownsensor2values[NUMCHANNELS]; //buffer to keep sensor2 readings in
 
    while (1)
    {
        g_sf_message0.p_api->pend(g_sf_message0.p_ctrl, &computation_thread_message_queue,
                             &pSensorDataHeader, TX_NO_WAIT); //if a message has been posted to the queue, store its address in pSensorDataHeader
 
        if (pSensorDataHeader->event_b.class_code == SF_MESSAGE_EVENT_CLASS_SENSORDATA) //if the message is the right kind
        {
            pSensorDataPayload = (sensordata_payload_t *) pSensorDataHeader; //cast the received message to the custom type
            //store the sensor information in some buffers, this part is application dependent
            if (pSensorDataPayload->header.event_b.code == SF_MESSAGE_EVENT_NEW_DATA) //if the message event is the right kind
            {
                if (pSensorDataPayload->header.event_b.class_instance ==0) //from sensor 1
                {
                    for (uint8_t temp=0; temp<NUMCHANNELS;temp++) lastknownsensor1values[temp] = pSensorDataPayload->sensordata[temp];
                }
                if (pSensorDataPayload->header.event_b.class_instance ==1) //from sensor 2
                {
                    for (uint8_t temp=0; temp<NUMCHANNELS;temp++) lastknownsensor2values[temp] = pSensorDataPayload->sensordata[temp];
                }
                g_sf_message0.p_api->bufferRelease(g_sf_message0.p_ctrl, pSensorDataHeader, SF_MESSAGE_RELEASE_OPTION_NONE);
            }
        }
 
        //send a message about the most recent sensor data
        errorBuffPost = g_sf_message0.p_api->bufferAcquire(g_sf_message0.p_ctrl, &pOutputBuffer, &acquireCfgPost, 300); //attempt to acquire the posting buffer
        if (errorBuffPost==SSP_SUCCESS)
        {
            pOutputPost = (sensordata_payload_t *) pOutputBuffer; //cast buffer to our payload
            pOutputPost->header.event_b.class_code = SF_MESSAGE_EVENT_CLASS_SENSORDATA; //set the event class
            pOutputPost->header.event_b.class_instance = 2; //set the class instance
            pOutputPost->header.event_b.code = SF_MESSAGE_EVENT_NEW_DATA; //set the message type
            for (uint8_t temp=0; temp<NUMCHANNELS;temp++) pOutputPost->sensordata[temp] = lastknownsensor1values[temp] + lastknownsensor2values[temp];
            //for this demo, just add the two sensors read values
            for (uint8_t temp=0;temp<NUMCHANNELS;temp++) pOutputPost->sensorchannel[temp] = temp;
            g_sf_message0.p_api->post(g_sf_message0.p_ctrl, (sf_message_header_t *) pOutputPost,
                                                          &post_cfg, &errPost, TX_WAIT_FOREVER); //post the message
        }
        tx_thread_sleep (50);
    }
}

最后,下方给出了输出线程的实现。它可能看起来与之前所有教程中使用的类似,但这次在调用UART写入时增加了阻塞特性。这确保了在释放缓冲区之前,UART设备已完成从消息中写入数据。如果不这样做,就有可能覆盖UART模块正在读取但尚未完成输出的数据!显然,这不是期望的行为。

按照当前的配置方式,如果消息的发布速度快于UART的写入速度,缓冲区可能会溢出并导致程序挂起。如果线程不阻塞直到UART完成写入,消息缓冲区会在写入开始后立即释放。若控制线程在UART模块完成数据写入前发布了新消息,新消息将覆盖旧消息,导致UART模块从旧数据的位置开始写入新数据。

output_thread_entry.c

#include "output_thread.h"
#include "sensordata_api.h"
#include <stdio.h>
 
/* Output Thread entry function */
static volatile uint8_t uartdone=0;
 
void user_uart_callback(uart_callback_args_t *p_args)
{
    if (p_args->event == UART_EVENT_TX_COMPLETE)
        uartdone=1;
    return;
}
 
void output_thread_entry(void)
{
    uint8_t cstr[18*NUMCHANNELS];// = "Channel X: 12345\n"; //the text to be sent, stored as unsigned 8 bit data.
 
    g_uart0.p_api->open(g_uart0.p_ctrl, g_uart0.p_cfg); //initialization of the UART module
 
    sf_message_header_t * pHeader; //pointer to the message header
    sensordata_payload_t * thepayload; //pointer to the message payload
 
    while (1)
    {
        g_sf_message0.p_api->pend(g_sf_message0.p_ctrl, &output_thread_message_queue,
                                  &pHeader, TX_WAIT_FOREVER); //wait for a message forever
 
        if (pHeader->event_b.class_code == SF_MESSAGE_EVENT_CLASS_SENSORDATA) //if the message if the right kind
        {
            thepayload = (sensordata_payload_t *) pHeader; //cast the received message to the custom type
            if (thepayload->header.event_b.code == SF_MESSAGE_EVENT_NEW_DATA) //if the message event is the right kind
            {
                //spit out to UART
                for (uint8_t index=0; index<NUMCHANNELS; index++)
                {
                    sprintf(cstr +index*18, "Channel %1c: %5d\n", thepayload->sensorchannel[index]+'0', thepayload->sensordata[index]);
                }
                g_uart0.p_api->write(g_uart0.p_ctrl, cstr, 18*NUMCHANNELS); //send the information over UART
                while(uartdone==0); //block until uart completes
                uartdone=0;
                g_sf_message0.p_api->bufferRelease(g_sf_message0.p_ctrl, pHeader, SF_MESSAGE_RELEASE_OPTION_NONE);
            }
        }
        tx_thread_sleep (1);
    }
}

细心的读者可能已经注意到hal_entry.c文件未被包含在内。其默认内容对本应用而言已经足够。

输出 / 结论

如上图所示,该程序将两组数据相加生成第三组数据,并通过UART发送这第三组数据。这是一个简单的演示程序,展示了如何在瑞萨消息框架下使用多消息源线程。

本指南为多消息源程序提供了基本框架。示例代码具有足够的通用性,可扩展用于不同应用场景。如果您觉得有用,欢迎复制并修改它。

问题 / 评论

如有任何问题或意见,请访问得捷电子技术论坛