分类 嵌入式软件 下的文章

3轴加速度,Tri-Axis Accelerometer,G-sensor,有不同的说法,但是表达的意思是一样的,我们要实现物体移动检测,计步器,简单应用还是非常不错的,至于6轴或9轴,不在本文章覆盖范围。

准备工作

参考驱动

官方并没有给出完整的代码参考,不过在github找到了非常不错的示例,只不过代码是 linux 下的,需要移植到嵌入式需要一些工作。仓库地址:https://github.com/eclipse/upm ,完整的驱动在 /src/kxtj3 目录下,需要3个文件:kxtj3.c, kxtj3.h, kxtj3_registers.h。有一个 example 在 /examples/c/kxtj3.c 下。全部移植到你的工程代码目录中。

代码移植

example 文件分析

主要是根据自己的嵌入式平台,改一下适配。

#define SENSOR_ADDR 0x0f // 设备slave号,接口低电平是 0x0E,高电平是 0x0F
#define I2C_BUS 0  // 使用哪个 i2c
#define SAMPLE_COUNT 10 // 读取传感器次数

bool isStopped = false;

void signal_int_handler(int signo)
{
    if (signo == SIGINT)
        isStopped = true;
}

// 读取,打印
void print_acceleration_data(kxtj3_context dev)
{
    float wait_time = kxtj3_get_acceleration_sampling_period(dev) * SECOND_IN_MICRO_S;
    uint8_t sample_counter = 0;
    float x, y, z;
    while (sample_counter < SAMPLE_COUNT && !isStopped)
    {
        kxtj3_get_acceleration_data(dev, &x, &y, &z);
        printf("%.02f | %.02f | %.02f\n", x, y, z);
        usleep(wait_time);
        sample_counter++;
    }
}

int main(int argc, char **argv)
{
    signal(SIGINT, signal_int_handler);
    
    // i2c 初始化
    printf("Sensor init\n");
    kxtj3_context dev = kxtj3_init(I2C_BUS, SENSOR_ADDR);
    if (!dev)
    {
        printf("kxtj3_init() failed.\n");
        return -1;
    }

    // sensor 初始化
    printf("Setting settings:\nODR: 25 Hz\nResolution: High\nAcceleration range: 16g with 14bits");
    kxtj3_sensor_init(dev, KXTJ3_ODR_25, HIGH_RES, KXTJ3_RANGE_16G_14);
    printf("Showing acceleration data:\n");
    print_acceleration_data(dev);

    printf("Closing sensor\n");
    kxtj3_close(dev);
    return 0;
}

驱动文件分析

i2c 驱动相关

主要是 i2c 读取/写入的几个函数适配,其他函数不需要更改。

// Register Read/Write helper functions
static upm_result_t kxtj3_read_register(const kxtj3_context dev, uint8_t reg, uint8_t *data)
{
    int value = mraa_i2c_read_byte_data(dev->i2c, reg); // 适配字节读取,注意都是指针传递
    if (value == -1)
    {
        printf("%s: mraa_i2c_read_byte_data() failed.\n", __FUNCTION__);
        return UPM_ERROR_OPERATION_FAILED;
    }

    *data = (uint8_t)value;
    return UPM_SUCCESS;
}

static upm_result_t kxtj3_read_registers(const kxtj3_context dev, uint8_t reg, uint8_t *data, int len)
{
    if (mraa_i2c_read_bytes_data(dev->i2c, reg, data, len) != (int)len) // 适配多字节读取,注意都是这真传递
        return UPM_ERROR_OPERATION_FAILED;

    return UPM_SUCCESS;
}

static upm_result_t kxtj3_write_register(const kxtj3_context dev, uint8_t reg, uint8_t val)
{
    if (mraa_i2c_write_byte_data(dev->i2c, val, reg) != MRAA_SUCCESS) // 适配字节写入
    {
        printf("%s: mraa_i2c_write_byte_data() failed.\n", __FUNCTION__);
        return UPM_ERROR_OPERATION_FAILED;
    }

    return UPM_SUCCESS;
}

i2c 连接,其实就是指定你的 i2c 相关内容,可有可无,保留函数结构即可。

static bool kxtj3_check_mraa_i2c_connection(kxtj3_context dev, int bus, uint8_t addr)
{
    if (mraa_init() != MRAA_SUCCESS)
    {
        printf("%s: mraa_init() failed.\n", __FUNCTION__);
        kxtj3_close(dev);
        return false;
    }

    if (!(dev->i2c = mraa_i2c_init(bus)))
    {
        printf("%s: mraa_i2c_init() failed.\n", __FUNCTION__);
        kxtj3_close(dev);
        return false;
    }

    if (mraa_i2c_address(dev->i2c, addr))
    {
        printf("%s: mraa_i2c_address() failed.\n", __FUNCTION__);
        kxtj3_close(dev);
        return false;
    }

    return true;
}

初始化函数,很重要,这是入口,初始化了 kxtj3_context。

kxtj3_context kxtj3_init(int bus, uint8_t addr)
{
    kxtj3_context dev = (kxtj3_context)malloc(sizeof(struct _kxtj3_context));
    if (!dev)
        return NULL;

    dev->i2c = NULL;
    dev->interrupt_pin = NULL;

    if (!kxtj3_check_mraa_i2c_connection(dev, bus, addr)) // i2c 初始化
        return NULL;

    if (!kxtj3_check_who_am_i(dev)) // 读取设备号,必须返回 0x35 才能行
        return NULL;

    kxtj3_set_default_values(dev);

    kxtj3_set_odr_wakeup_function(dev, dev->odr_wakeup);
    kxtj3_sensor_init(dev, dev->odr, dev->res_mode, dev->g_range_mode);

    return dev;
}

中断唤醒MCU相关

自定义一个函数,配置中断触发的高低电平和清除模式,在系统初始化时调用即可。

upm_result_t kxtj3_interrupt_init(const kxtj3_context dev)
{
    kxtj3_enable_wakeup_interrupt(dev);
    kxtj3_enable_interrupt_pin(dev, ACTIVE_LOW, LATCH_UNTIL_CLEARED); // ACTIVE_LOW:低电平触发,LATCH_UNTIL_CLEARED:手工清除

    kxtj3_set_wakeup_threshold_g_value(dev, 0.1);  // 0.1 代表了 0.1g 的加速度,0.1g 基本可以使能拿起来就触发

    return UPM_SUCCESS;
}

实验效果

读出来的效果,基本就是三个指标,xyz坐标轴的状态值,自己打印出来即可;
中断触发唤醒MCU,这个也不好演示,自己试试能不能唤醒MCU。

我们都知道蓝牙BLE设备只能与蓝牙相关设备通信,如果需要将蓝牙BLE设备接入物联网平台,那需要通过一些技术手段来实现协议和数据的转换。本文章,就是为这个解决方案提供一个指引。

准备工作

设备和软件相关

  • 蓝牙BLE广播设备,可以是一个iBeacon,这里用 YiTHE 人体温度计为例;
  • 蓝牙BLE网关,可以是一个通用的蓝牙网关或可以二次开发的网关;
  • ThingsBoard Gateway,需要二次开发;

蓝牙网关需要跟蓝牙设备之间已经建立连接和配合,具体配置要看蓝牙设备的协议是否被网关支持。

数据走向

  1. 蓝牙设备持续广播,将数据发送出来:02010613ff11aae6000055444757543035000000000000;
  2. 网关设备持续扫描广播,并解析广播数据,比如 YiTHE ;
  3. 蓝牙网关将数据发送到 ThingsBoard Gateway;
  4. ThingsBoard 端需要针对 iBeacon 数据解析,并入库。

协议转换

蓝牙网关端

为了区分自有设备,过滤掉其他厂家设备,需要在网关处开发过滤规则:

  • 根据广播数据长度过滤不相关数据;
  • 根据数据内容关键字过滤;

ThingsBoard 端

为了保证数据完整性,我们将原始数据直接发送到 TB ,然后,在规则链 中过滤并且分析数据,写入:

adv_data = msg.adv_data;

// 电池
battery_str = adv_data.substr(20, 2);
hex = "0x" + battery_str;
charValueBattery = String.fromCharCode(hex);
battery = charValueBattery.charCodeAt(0);
msg.battery = battery;

// 温度
temp_symbol = adv_data.substr(10, 2);
if (temp_symbol == "00") {
    symbol = -1;
} else if (temp_symbol == "11") {
    symbol = 1;
}
temp1_str = adv_data.substr(16, 2);
temp2_str = adv_data.substr(14, 2);
hex = "0x" + temp1_str + temp2_str;
charValueTemp = String.fromCharCode(hex);
temp = charValueTemp.charCodeAt(0);
msg.temp = symbol * temp / 10.0;

// 温度单位 摄氏度或华氏度
temp_unit_str = adv_data.substr(12, 2);
if (temp_unit_str == "aa") {
    unitCode = "C";
} else if (temp_unit_str == "bb") {
    unitCode = "F";
}
msg.temp_unit = unitCode;

// 按键标志
pressed_str = adv_data.substr(18, 2);
if (pressed_str == "01") {
    pressed = true;
} else if (pressed_str == "00") {
    pressed = false;
}
msg.pressed = pressed;

// 设备id
devcie_id_str = adv_data.substr(34, 12);
hex = "0x" + devcie_id_str;
charValueDeviceId = String.fromCharCode(hex);
device_id = charValueDeviceId.charCodeAt(0);
msg.device_id = device_id;

// 设备标志位
var result = [];
device_flag_str = adv_data.substr(22, 12);
for (var i = 0; i < device_flag_str.length / 2; i++) {
    var aa = device_flag_str.slice(i * 2, (i + 1) * 2);
    result.push(aa);
}

var device_flag = "";
for (var i = 0; i < result.length; i++) {
    device_flag += String.fromCharCode(parseInt(result[i],
        16));
}
msg.device_flag = device_flag;

return {
    msg: msg,
    metadata: metadata,
    msgType: msgType
};

需要解析数据,并写入msg,达到入库的作用。

{
    "msg": {
        "adv_data": "02010613ff11aae6000055444757543035000000000000",
        "battery": 85,
        "temp": 23,
        "temp_unit": "C",
        "pressed": false,
        "device_id": 0,
        "device_flag": "DGWT05"
    },
    "metadata": {
        "deviceType": "default",
        "deviceName": "Test Device",
        "ts": "1618149679239"
    },
    "msgType": "POST_TELEMETRY_REQUEST"
}

结果呈现

设备列表

yithe2.png

设备日志

yithe.png

产品特性

YiAIR-86-印刷-2.png

  • 无线远传:NB-IoT全网通
  • 干电池供电:2节AA电池(5号)
  • 静态功耗:< 20uA,可用1年*
  • 温/湿度精度:±0.2℃/±3%
  • 私有化 IoT 平台:多租户管理
  • 尺寸:86 x 86 x 30mm

适用范围

  • 供暖、冷链、厂房、仓库
  • 办公室、会议室、学校、幼儿园
  • 酒店、医院、商场、咖啡厅
  • 酒吧、餐馆、地铁、地下室

多租户

多租户.png

仪表板

dashboard.png

Google Protocol Buffers ,一个非常高效的数据序列化工具,ava/Python/C++/C#等都是完美支持,可惜了,官方不支持C语言,还好世界强大,有人做了C语言版本:https://github.com/protobuf-c/protobuf-c

关于protobuf,请自行google或百度查询,这里不再啰嗦,进入正题。

1. 第一步:安装Protocol Buffers

https://developers.google.com/protocol-buffers/
很简单,download下来,编译即可;make && make install

2. 第二步:安装protobuf-c

https://github.com/protobuf-c/protobuf-c
也是很简单,download下来,编译即可;make && make install

当你输入 protoc-c 出现以下信息,代表安装成功。

jiekechoo:mqtt_protoc jiekechoo$ protoc-c 
Usage: protoc-c [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
  -IPATH, --proto_path=PATH   Specify the directory in which to search for
                              imports.  May be specified multiple times;
                              directories will be searched in order.  If not
                              given, the current working directory is used.
                              If not found in any of the these directories,
                              the --descriptor_set_in descriptors will be
                              checked for required proto file.
  --version                   Show version info and exit.
  -h, --help                  Show this text and exit.
  --encode=MESSAGE_TYPE       Read a text-format message of the given type
                              from standard input and write it in binary
                              to standard output.  The message type must
                              be defined in PROTO_FILES or their imports.
  --decode=MESSAGE_TYPE       Read a binary message of the given type from
                              standard input and write it in text format
                              to standard output.  The message type must
                              be defined in PROTO_FILES or their imports.
  --decode_raw                Read an arbitrary protocol message from
                              standard input and write the raw tag/value
                              pairs in text format to standard output.  No
                              PROTO_FILES should be given when using this
                              flag.
  --descriptor_set_in=FILES   Specifies a delimited list of FILES
                              each containing a FileDescriptorSet (a
                              protocol buffer defined in descriptor.proto).
                              The FileDescriptor for each of the PROTO_FILES
                              provided will be loaded from these
                              FileDescriptorSets. If a FileDescriptor
                              appears multiple times, the first occurrence
                              will be used.
  -oFILE,                     Writes a FileDescriptorSet (a protocol buffer,
    --descriptor_set_out=FILE defined in descriptor.proto) containing all of
                              the input files to FILE.
  --include_imports           When using --descriptor_set_out, also include
                              all dependencies of the input files in the
                              set, so that the set is self-contained.
  --include_source_info       When using --descriptor_set_out, do not strip
                              SourceCodeInfo from the FileDescriptorProto.
                              This results in vastly larger descriptors that
                              include information about the original
                              location of each decl in the source file as
                              well as surrounding comments.
  --dependency_out=FILE       Write a dependency output file in the format
                              expected by make. This writes the transitive
                              set of input file paths to FILE
  --error_format=FORMAT       Set the format in which to print errors.
                              FORMAT may be 'gcc' (the default) or 'msvs'
                              (Microsoft Visual Studio format).
  --print_free_field_numbers  Print the free field numbers of the messages
                              defined in the given proto files. Groups share
                              the same field number space with the parent 
                              message. Extension ranges are counted as 
                              occupied fields numbers.

  --c_out=OUT_DIR             Generate C/H files.
  @<filename>                 Read options and filenames from file. If a
                              relative file path is specified, the file
                              will be searched in the working directory.
                              The --proto_path option will not affect how
                              this argument file is searched. Content of
                              the file will be expanded in the position of
                              @<filename> as in the argument list. Note
                              that shell expansion is not applied to the
                              content of the file (i.e., you cannot use
                              quotes, wildcards, escapes, commands, etc.).
                              Each line corresponds to a single argument,
                              even if it contains spaces.

3. 测试protobuf

转换proto文件为 C 语言库

protoc-c --c_out=. kurapayload.proto

生成protoc文件

-rw-r--r--  1 jiekechoo  staff  14836  8 18 11:36 kurapayload.pb-c.c
-rw-r--r--  1 jiekechoo  staff   6427  8 18 11:36 kurapayload.pb-c.h
-rw-r--r--  1 jiekechoo  staff   1749  8 18 11:17 kurapayload.proto

完成protobuf集成工作

#include <stdio.h>
#include <stdlib.h>
#include "MQTTClient.h"
#include "kurapayload.pb-c.h"
 
void main()
{
MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_message pubmsg = MQTTClient_message_initializer;
MQTTClient_deliveryToken token;
int rc;

MQTTClient_create(&client, ADDRESS, CLIENTID,
    MQTTCLIENT_PERSISTENCE_NONE, NULL);
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
conn_opts.username = "username";
conn_opts.password = "password";

if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
    printf("Failed to connect, return code %d\n", rc);
    exit(-1);
}
    Kuradatatypes__KuraPayload payload = KURADATATYPES__KURA_PAYLOAD__INIT;
    Kuradatatypes__KuraPayload__KuraMetric **metrics;
    Kuradatatypes__KuraPayload__KuraPosition position = KURADATATYPES__KURA_PAYLOAD__KURA_POSITION__INIT;
 
position.latitude = 1.1;
position.longitude = 1.1;

payload.position = &position;

metrics = malloc(sizeof (Kuradatatypes__KuraPayload__KuraMetric) * 2);
int i = 0;
for(i=0;i<2;i++)
{
    metrics[i] = malloc (sizeof (Kuradatatypes__KuraPayload__KuraMetric));
    kuradatatypes__kura_payload__kura_metric__init(metrics[i]);

    metrics[i]->name = (char*)EdcBirthPayloadNames[i];
    printf("%s, ", metrics[i]->name);
    metrics[i]->type = KURADATATYPES__KURA_PAYLOAD__KURA_METRIC__VALUE_TYPE__STRING;        
    metrics[i]->string_value = (char*)EdcBirthPayloadValues[i];
    printf("%s\r\n", metrics[i]->string_value);
}
    payload.n_metric = 2;
    payload.metric = metrics;

    size_t len = kuradatatypes__kura_payload__get_packed_size(&payload);
    void* data = malloc(len); 
    kuradatatypes__kura_payload__pack(&payload, data);
    pubmsg.payload = data;
    pubmsg.payloadlen = kuradatatypes__kura_payload__get_packed_size(&payload);
    MQTTClient_publishMessage(client, BIRTH_TOPIC, &pubmsg, &token);

}

编译,如果没有出错就上成功。

gcc kurapayload.pb-c.c main.c -lprotobuf-c

4. 注意事项

  • 系统要能够使用 malloc 功能,好多地方要用到;
  • 结构体初始化 INIT 或 使用 init 函数;
  • 数据准备完毕,要 pack 一下,再 packed_size 计算长度,后面该怎么处理都行了;

最终,C 语言版本的 Kura 模拟器成功连接 Kapua。
1761599121932_.pic_hd.jpg

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

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