2020年7月

GD32 兆易创新单片机,国产芯,必须支持!完美兼容 ARM 系列其他产品,价格美丽,性能卓越,支持到位。
freeModbus,开源且免费的 Modbus slave 实现,支持各种平台,包括嵌入式单片机系统。

1. 准备 GD32 系列单片机设备,本文采用 GD32F130F8;

你需要一块带有 GD32 单片机的板子,可以说开发板或成品设备,电路正常,带有串口 USART,最好具备 RS485 电路;用官方的 GD32F1x0_Firmware_Library_V3.0.0 example 新建项目,编译成功,且下载到设备,能够运行跑马灯,串口能打印正常。

  • 串口输出正常,波特率正常,比如 USART0
  • 串口中断输入正常,收到外部串口输入能及时处理
  • 定时器TIMER正常,需要 us 微秒级控制
  • GPIO输出正常,能够控制高低电平

2、准备 freeModbus V1.6版本,V1.5 也可以;

作为开发者移植 Modbus,相关原理最好还是要熟悉一下:https://www.modbus.org/, 不管怎么样,通过你自己的方式熟读一下 Modbus 协议内容。
github下载 freeModbus, 给作者 star,fork 到自己的项目;

  • modbus 目录结构
    原封不动,搬进你的工程中

    ├── ascii // ASCII模式文件,本例程未用到
    │   ├── mbascii.c
    │   └── mbascii.h
    ├── functions // 功能函数模块,很重要,不需要修改
    │   ├── mbfunccoils.c
    │   ├── mbfuncdiag.c
    │   ├── mbfuncdisc.c
    │   ├── mbfuncholding.c
    │   ├── mbfuncinput.c
    │   ├── mbfuncother.c
    │   └── mbutils.c
    ├── include  // 头文件,很重要,不需要修改
    │   ├── mb.h
    │   ├── mbconfig.h
    │   ├── mbframe.h
    │   ├── mbfunc.h
    │   ├── mbport.h
    │   ├── mbproto.h
    │   └── mbutils.h
    ├── mb.c // 主文件,不需要修改
    ├── rtu // RTU 模块文件,很重要,本例程需要熟读内容
    │   ├── mbcrc.c
    │   ├── mbcrc.h
    │   ├── mbrtu.c
    │   └── mbrtu.h
    └── tcp // TCP 模块,本例程未用到
      ├── mbtcp.c
      └── mbtcp.h
  • 移植要求
    主要是针对 port 目录下的移植

    ├── demo.c // 主函数移植内容
    └── port // 移植目录
      ├── port.h // 头文件
      ├── portevent.c // 移植事件文件,本例程不修改
      ├── portserial.c // 移植串口处理,很重要,需要熟读
      └── porttimer.c // 移植定时器处理,很重要,需要熟读

3、移植过程;

main.c

使用上一步中的 demo/BARE/port 下所有文件导入 GD32 工程中,且加入所有的 .h 文件到编译目录;将 demo.c 中的主循环内容移植到你的工程主循环中,波特率,串口号,等等参数,自行根据需要修改。

    eMBErrorCode    eStatus;
    eStatus = eMBInit( MB_RTU, 0x03, 0, 9600, MB_PAR_NONE );
    /* Enable the Modbus Protocol Stack. */
    eStatus = eMBEnable(  );
    for( ;; )
    {
        ( void )eMBPoll(  );
    }

port.c

在工程中新建一个 port.c , 把 modbus 回调函数填入,这里只是主要内容,其他默认回调也必须存在

    /* ----------------------- Defines ------------------------------------------*/
    #define REG_HOLDING_START ( 0x0000 )
    #define REG_HOLDING_NREGS ( 4 )
    
    /* ----------------------- Static variables ---------------------------------*/
    static USHORT   usRegHoldingStart = REG_HOLDING_START;
    static USHORT   usRegHoldingBuf[REG_HOLDING_NREGS] = { 0xAAAA, 0xBBBB, 0xCCCC, 0xDDDD};

    eMBErrorCode
    eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
    {
        /* error state */
        eMBErrorCode    eStatus = MB_ENOERR;
        /* offset */
        int16_t iRegIndex;
        
        /* test if the reg is in the range */
        if (((int16_t)usAddress-1 >= REG_HOLDING_START) 
            && (usAddress-1 + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS))
        {
            /* compute the reg's offset */
            iRegIndex = (int16_t)(usAddress-1 - REG_HOLDING_START);
            switch (eMode)
            {
                case MB_REG_READ:
                    while (usNRegs > 0)
                    {
                        *pucRegBuffer++ = (uint8_t)( usRegHoldingBuf[iRegIndex] >> 8 );
                        *pucRegBuffer++ = (uint8_t)( usRegHoldingBuf[iRegIndex] & 0xff);
                        iRegIndex ++;
                        usNRegs --;
                    }
                    break;
                case MB_REG_WRITE:
                    while (usNRegs > 0)
                    {
                        usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
                        usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
                        iRegIndex ++;
                        usNRegs --;
                    }
                    break;
                    
            }
        }
        else{
            eStatus = MB_ENOREG;
        }
        
        return eStatus;
    }

增加全局中断函数使能/失能调用,这里用 CMSIS 通用配置

void
EnterCriticalSection( void )
{
  __disable_irq();
}

void
ExitCriticalSection( void )
{
  __enable_irq();
}

portserial.c

串口使能/失能配置【很重要】

/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    /* If xRXEnable enable serial receive interrupts. If xTxENable enable
     * transmitter empty interrupts.
     */
    if (xRxEnable)
    {
      gd_eval_ledoff(LED3); // 输入模式,设置 RS485 芯片 RE 口低电平
      usart_interrupt_enable(EVAL_COM1, USART_INT_RBNEIE); // 打开输入中断
    }
    else
    {
      gd_eval_ledon(LED3); // 输出模式,设置 RS485 芯片 RE 口低电平
      usart_interrupt_disable(EVAL_COM1, USART_INT_RBNEIE); // 关闭输入中断
    }
    
    if(xTxEnable)
    {
      usart_interrupt_enable(EVAL_COM1, USART_INT_TCIE); // 打开输出完成中断
    }
    else
    {
      usart_interrupt_disable(EVAL_COM1, USART_INT_TCIE); // 关闭输出完成中断
    }
}

串口初始化配置

BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
    /* USART interrupt configuration */
    nvic_irq_enable(USART0_IRQn, 0, 2);  // 串口中断号设置
     
    gd_eval_COMinit(EVAL_COM1); // 初始化串口
 
    usart_interrupt_enable(EVAL_COM1, USART_INT_RBNEIE); // 初始化后好就打开串口输入中断
    gd_eval_ledoff(LED3); // 485 芯片 RE 口低电平输入模式
  
    return TRUE;
}

串口输出/输入单字节操作【很重要,也很简单】

BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
    /* Put a byte in the UARTs transmit buffer. This function is called
     * by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
     * called. */
    usart_data_transmit(EVAL_COM1, ucByte);
    return TRUE;
}

BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
    /* Return the byte in the UARTs receive buffer. This function is called
     * by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
     */
   *pucByte = usart_data_receive(EVAL_COM1); 
    return TRUE;
}

串口中断回调函数【非常重要,非常重要,非常重要】

void USART0_IRQHandler(void)
{
    if(RESET != usart_interrupt_flag_get(EVAL_COM1, USART_STAT_RBNE, USART_INT_RBNEIE)){
      /* receive data */
      prvvUARTRxISR();
    }
    
    if(RESET != usart_flag_get(EVAL_COM1, USART_STAT_TC)){
      /* transmit data */
      prvvUARTTxReadyISR();
    }
}

其他内容不需要修改。

porttimer.c

此文件主要是定时器设置,关于这个定时器,很多文章有写到,最关键是 50us 这个要相对准确

定时器初始化,说多了都是泪,自己体会,照抄下面的代码即可,注意使用哪个定时器TIMER1,MCU 主频要换算成 50us

BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
    timer_parameter_struct timer_initpara;

    rcu_periph_clock_enable(RCU_TIMER1);

    timer_deinit(TIMER1);

    /* TIMER1 configuration */
    timer_initpara.timer_prescaler         = 3599;  // 72MHz, 注意,这样就是50us
    timer_initpara.timer_alignedmode       = TIMER_COUNTER_EDGE;
    timer_initpara.timer_counterdirection  = TIMER_COUNTER_UP;
    timer_initpara.timer_period            = usTim1Timerout50us;
    timer_initpara.timer_clockdivision     = TIMER_CKDIV_DIV1;
    timer_initpara.timer_repetitioncounter = 0;
    timer_init(TIMER1,&timer_initpara);
    
    /* TIMER0 channel control update interrupt enable */
    timer_interrupt_enable(TIMER1,TIMER_INT_UP);
    
    /* TIMER1 counter enable */
    timer_enable(TIMER1);
  
    // NVIC CONFIG
    nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);
    nvic_irq_enable(TIMER1_IRQn, 1, 1);
    
    return TRUE;
}

定时器使能/失能配置【很简单,但是很重要,尤其是计数器要清零】

inline void
vMBPortTimersEnable(  )
{
    /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
  
    timer_interrupt_flag_clear(TIMER1,TIMER_INT_UP);
    timer_counter_value_config(TIMER1, 0); // 清零
    /* TIMER1 counter enable */
    timer_enable(TIMER1);  
}

inline void
vMBPortTimersDisable(  )
{
    /* Disable any pending timers. */
  
    timer_counter_value_config(TIMER1, 0); // 清零
    /* TIMER1 counter enable */
    timer_disable(TIMER1);
}

定时器中断回调函数【很重要,但是很简单】

void TIMER1_IRQHandler(void)
{
    prvvTIMERExpiredISR();
    timer_interrupt_flag_clear(TIMER1,TIMER_INT_UP); // 清除定时器中断标志位
}

定时器其他照旧即可

portevent.c

没什么修改的

port.h

需要特别注意,修改下面两行

#define ENTER_CRITICAL_SECTION( )   EnterCriticalSection()
#define EXIT_CRITICAL_SECTION( )    ExitCriticalSection()

4、结果验证;

本例程只实现了 RTU 功能号 03 [保持寄存器读取],其他功能请自行扩展!
modbus-pull.PNG

有任何问题,请留言,我们共同讨论。

Eclipse Kura 作为边缘计算网关应用套件,功能非常强大,作为一个基础性框架,在上面跑一些用户应用,那是非常合适的。

一、启用 Kura 上面支持 MQTT broker
进入 Kura web console,开启Simple Artemis MQTT Broker,测试中暂定 usename 和 password 都为 mqtt。

WX20200707-174344.png

登录 Kura 设备后台,查看 TCP 1833 端口已经开启

root@raspberrypi:/home/pi# netstat -nlpt |grep 1883
tcp        0      0 0.0.0.0:1883            0.0.0.0:*               LISTEN      427/java            

二、模拟 MQTT 设备发送数据
使用 Chrome 浏览器插件 MQTTBox 登录我们的 MQTT server
WX20200707-174844@2x.png

三、安装 Kura 插件
使用 Eclipse marketplace 安装以下插件

  • Apache Camel MQTT endpoint
  • Apache Camel Groovy language support
  • Apache Camel GSON data format

四、创建新的 Component
创新 component
Integrating-with-Kura-and-Kapua-5.png

并输入以下内容

<routes xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="paho:humidity/sensor1/humidity?brokerUrl=tcp://localhost:1883&amp;clientId=route1&amp;userName=mqtt&amp;password=mqtt"/>
    <unmarshal><json library="Gson"></json></unmarshal>
    <transform><simple>${body["humidity"]}</simple></transform>
    <transform><groovy>["HUMIDITY": request.body/100, "ASSETNAME": "HrY", "SENSOR": "sensor1"]</groovy></transform>
    <to uri="seda:wiresOutput1"/>
  </route>
</routes>

WX20200707-175458.png

五、创建一个 Cloud publisher
WX20200707-175640.png

六、新建一个 Wire Graph
Camel Consumer - camel_comsumer
WX20200707-175756.png

Publisher - pub_camel1
WX20200707-175907.png

把两个相连起来
WX20200707-180007.png

七、发送数据,测试 Kapua 数据接收情况
发布/订阅 主题topic: humidity/sensor1/humidity
WX20200707-180254@2x.png

发送数据验证
WX20200707-180510@2x.png

Kapua 接收,查询结果
WX20200707-180442.png

Apache Camel是一个基于规则路由和中介引擎,提供企业集成模式的Java对象的实现,通过应用程序接口来配置路由和中介的规则。领域特定语言意味着Apache Camel支持你在的集成开发工具中使用平常的,类型安全的,可自动补全的Java代码来编写路由规则,而不需要大量的XML配置文件。 来自维基百科

网站: https://camel.apache.org

Kura_Camel_Integration.png

根据上图所示,Apache Camel 与 Apache Kura 集成,可以使得 Kura 扩展更多的功能,完成更多的工作。通过简单的配置,或少量的代码,就可以实现数据路由和中转。

Kapua Getting Start

https://www.eclipse.org/kapua/getting-started.php

Kapua docker 应用启动后,数据都在docker容器中,一旦重启容器,数据将丢失。

经过分析,需要将两部分的数据持久化到宿主主机磁盘上。

分两步:
1、启动docker compose实例,将sql容器中的数据拷贝出来,ca58ae61b875 是sql 容器的id;

docker cp ca58ae61b875:/var/opt/h2/data/kapuadb.mv.db .

2、修改docker-compose.xml,在特定位置增加下面volumes, 在映射宿主主机文件系统到docker 容器;

  db:
    volumes:
      - ./sql:/var/opt/h2/data
es:
    volumes:
      - ./es:/usr/share/elasticsearch/data

至此,Kapua 测试环境就可以当做准生产环境了。

systick.c
定义1Hz

/*!
    \brief      configure systick
    \param[in]  none
    \param[out] none
    \retval     none
*/
void systick_config(void)
{
    /* setup systick timer for 1Hz interrupts */
    if (SysTick_Config(SystemCoreClock / 1000 / 1000)){
        /* capture error */
        while (1);
    }
    /* configure the systick handler priority */
    NVIC_SetPriority(SysTick_IRQn, 0x00);
}

阻塞式延时函数

/*!
    \brief      delay a time in milliseconds
    \param[in]  count: count in milliseconds
    \param[out] none
    \retval     none
*/
void delay_1us(uint32_t count)
{
    delay = count;

    while(0 != delay);
}

void delay_1ms(uint32_t count)
{
    delay = count * 1000;

    while(0 != delay);
}

gd32f1x0_it.c
中断处理函数中调用led_spark(),实现led翻转

/*!
    \brief      this function handles SysTick exception
    \param[in]  none
    \param[out] none
    \retval     none
*/
void SysTick_Handler(void)
{
    led_spark();
    delay_decrement();
}

main.c
led 翻转函数

/*!
    \brief      toggle the led every 500ms
    \param[in]  none
    \param[out] none
    \retval     none
*/
void led_spark(void)
{
    static __IO uint32_t timingdelaylocal = 0;

    if(timingdelaylocal){

        if(timingdelaylocal < 500*1000){
            gd_eval_ledon(LED2);
        }else{
            gd_eval_ledoff(LED2);
        }

        timingdelaylocal--;
    }else{
        timingdelaylocal = 1000*1000;
    }
}

日常阻塞式翻转

int main(void)
{
    gd_eval_ledinit(LED2);

    systick_config();
    
    while(1)
    {
      gd_eval_ledon(LED2);
      delay_1us(500*1000);
      gd_eval_ledoff(LED2);
      delay_1us(200*1000);
    }
}