FreeRTOS记录(十、FreeRTOS实现带 I2C 通讯的 ModbusRTU 协议从机实例) 您所在的位置:网站首页 中断RTOS FreeRTOS记录(十、FreeRTOS实现带 I2C 通讯的 ModbusRTU 协议从机实例)

FreeRTOS记录(十、FreeRTOS实现带 I2C 通讯的 ModbusRTU 协议从机实例)

2024-07-05 22:49| 来源: 网络整理| 查看: 265

还是一个FreeRTOS的例子,这次不是裸机工程转的,没有大部分复制的代码, 所以会把步骤会记录详细一点,这应该也是博文中 FreeRTOS 最后一个例子了 平台: STM32L051C8T6 欧姆龙 D6T 红外测温传感器 I2C 协议 设备作为485从机

目录 一、STM32CubeMX 创建工程1.1 芯片基本设置1.2 FreeRTOS基本设置 二、基本框架代码添加2.1 一些习惯的typedef 和 宏定义2.2 各外设相关代码2.2.1 定时器(逻辑处理和按键驱动计时)2.2.2 串口(调试串口和485通讯串口)2.2.3 按键 三、各外设函数代码3.1 485从机驱动(ModbusRTU协议)3.1.1 寄存器地址的疑问3.1.2 485 驱动测试 3.2 D6T传感器驱动(I2C协议)3.2.1 通用I2C驱动3.2.2 I2C传感器数据读取 3.3 任务调整、任务通讯3.3.1 任务栈说明3.3.2 任务间通信

本产品的功能就是通过红外测温传感器定时测量温度保存,设备通过RS484接口,使用ModbusRTU协议进行传输,设备作为从机接收主机的查询上报温度等其他数据。

具体的理论分析请参考博文 MODBUS RTU 485 部分:总线协议记录(I2C,SPI, Modbus 485, CAN…)

一、STM32CubeMX 创建工程

第一步创建工程,这个以前博文中就提到过很多次了,实在不会去看一下我FreeRTOS记录(九、一个裸机工程转FreeRTOS的实例)中的说明,里面的 2.1 基本框架搭建 有所有相关的博文地址。

这里我就简单放一个步骤,说明一下我的基本设置:

1.1 芯片基本设置 GPIO口,按键,LED,一个I2C传感器; 在这里插入图片描述系统时钟源,使用外部高速时钟: 在这里插入图片描述基础时钟源和调试方式,不勾选 Debug Serial Wire 烧录一次就会锁死,不能正常烧录,需要手动操作: 在这里插入图片描述系统时钟频率,使用的STM32L051 ,最大支持32MHz,这里就选32MHz: 在这里插入图片描述定时器设置,根据自己的使用习惯,两个定时器,一个定时器做基本逻辑时间管理,一个用于按键驱动的定时器: 在这里插入图片描述在这里插入图片描述串口设置,2个串口,一个用于打印的调试串口1,一个用于485通讯的串口: 在这里插入图片描述在这里插入图片描述 1.2 FreeRTOS基本设置 任务和消息队列的创建,要考虑到设备的主要功能有哪些,传感器读取,485通讯,按键用来做一些观察测试(按键其实可以不用,但是考虑也可以通过按键来设置485的地址,这个想法再看): 在这里插入图片描述打开任务运行情况查询功能,确保实施观察任务运行状态,这个东西熟悉了以后或者等系统稳定以后可以去掉: 在这里插入图片描述在这里插入图片描述然后就可以生成代码了,用什么环境选什么,我用的是 gcc 环境,这里选用的是 Makefile ,这里最后我还有个小操作,把堆空间改小了一点: 在这里插入图片描述 二、基本框架代码添加 2.1 一些习惯的typedef 和 宏定义

根据个人习惯,把数据类型的名称命名头文件加入工程,在main.h文件中包含,这样在写代码的时候一些就可以根据个人习惯使用数据类型了: 在这里插入图片描述

接下来对LED灯,和I2C引脚进行需要的宏定义: 在这里插入图片描述

2.2 各外设相关代码 2.2.1 定时器(逻辑处理和按键驱动计时)

定义两个全局变量,分别计算 定时器2 和 定时器 21的次数: 在这里插入图片描述 对于定时器21,直接计数就可以,后面在按键驱动中使用Timer21_count的值: 在这里插入图片描述 对于定时器2,在stm32l0xx_it.c文件中,进入定时器中断就处理,因为这里可能需要在中断中发送FreeRTOS任务通知或者消息等: 在这里插入图片描述

2.2.2 串口(调试串口和485通讯串口)

首先打印串口使用USART1,printf重定义,然后就能直接使用printf了: 在这里插入图片描述 接下来485通讯串口 USRAT2,我们需要使用 FreeRTOS 消息队列来发送通讯消息,需要在中断处理函数中操作: 在这里插入图片描述 上面是把串口接收到的消息放入消息队列,我们在 freertos.c文件中,把对应的接收消息队列的部分框架搭建好,同时自己要建立一个缓存用来保存消息队列的数据: 在这里插入图片描述 在StartModbusTask任务中: 在这里插入图片描述

完成上面的框架,别忘了打开串口接收中断,但是要注意,因为使用了消息队列接收,打开中断需要等FreeRTOS 初始化完了以后,否者会出现问题!

如下图,我们在MX_FREERTOS_Init函数中结尾处,打开串口2 接收中断: 在这里插入图片描述

2.2.3 按键

按键的话,我还是使用以前的那个驱动代码,就直接复制过来,修改一下定时器源:

几个实用的按键驱动

在这里插入图片描述 增加了.c 文件记得在Makefile中也添加一下: 在这里插入图片描述

三、各外设函数代码 3.1 485从机驱动(ModbusRTU协议)

具体的理论分析请参考博文 MODBUS RTU 485 部分:总线协议记录(I2C,SPI, Modbus 485, CAN…)

先把CRC16校验的文件包含进来,这个网上查一下就有,这里是我一直用的:ModBus-check.zip 在这里插入图片描述 然后写一下命令处理函数: 在这里插入图片描述 最后实现一下Modbus_03_ack函数。

3.1.1 寄存器地址的疑问

在写驱动代码的时候,忽然想到一个问题,寄存器地址该如何定义?

假如我们就从 0x0000 开始,然后依次+1 的定义寄存器,通过一个数组 test[n]是可以一一对应的,这个没问题。

寄存器地址数组0000Htest[0]0001Htest[1]0002Htest[2]……00XYtest[XY]

但是看各厂家有些设备,地址不是连续的,而且有小的有大的,比如: 在这里插入图片描述 上面这种情况总不能定义一个这么大的数组 test[0x0101]

(后续补充说明后来想想这个好像也不大,257,作为一款产品,肯定是厂家会保证产品地址在一定的范围内,然后可以用一个数组顺序对应寄存器地址,即便有多个通道,也是会用不同的数组表示,在一定的范围内可以离散,但是差距太大的话就得分不同数组)

当然,即使这样,我们依旧可以按照如下对应:

寄存器地址数组0000Htest[0]0001Htest[1]0006Htest[2]0100Htest[3]0101Htest[4]

但是问题是,看到设备的说明里面,从地址0000H 开始读取数据,读取7个长度的数据,0001H 到 0006H 的数据都会返回 0,这样子的话,单单用一个数组就不好处理了: 在这里插入图片描述

暂时想不出来 一个数组如何才能知道 他是否是连续的寄存器地址呢?不确定是不是还有其他的设定?(答案应该是上面红色部分)

当然,我考虑过使用 结构体,因为结构体的话,可以先针对的判断寄存器的ID,然后再处理数据,比如: 在这里插入图片描述 但是使用结构体数据处理相对来说 就没有数组那么简单,其实最主要的,关键还是在于产品定义的寄存区是否连续,如果连续的话,处理起来都没问题。

所以目前这个示例我还是把地址设置为连续的,通过一个数组,人为的定义好对应关系。

这里就直接上源码,把Modbus_rtu.c的源码都放上来:

#include "Modbus_rtu.h" #include "stdio.h" void Modbus_check() { u16 crc; u16 receivecrc1; u16 receivecrc2; u8 sendbuff[5]; crc = Checksum_CRC16(USART2_BUF,USART2_Data - 2); printf("crc is :0x%x\r\n",crc); /*No matter the high bits before or the low bits before*/ receivecrc1 = (USART2_BUF[USART2_Data - 2] if((crc == receivecrc2)||(crc == receivecrc1)){ switch (USART2_BUF[1]){ case 3: Modbus_03_ack(); break; case 6: Modbus_06_ack(); break; default: printf("An unsupported command!\r\n");//for test break; } } else{ //校验错误,返回异常 sendbuff[0] = mymodbus_add; sendbuff[1] = 0x80 | USART2_BUF[1]; sendbuff[2] = 0; crc = Checksum_CRC16(sendbuff,3); sendbuff[3] = (u8)(crc >> 8); sendbuff[4] = (u8)crc; Uart2_sendBuffer(sendbuff,5); } } } void Modbus_03_ack(){ u16 Register_add; // 2,3 u16 Register_len; // 4,5 u16 crc; u8 i; u8 j; Register_add = (USART2_BUF[2] case 0x0010: for(j=0;j sendbuff[i++]= (u8)(Register_value[1+j]>>8); sendbuff[i++]= (u8)Register_value[1+j]; } break; case 0x0012: for(j=0;j sendbuff[i++]= (u8)(Register_value[3+j]>>8); sendbuff[i++]= (u8)Register_value[3+j]; } break; case 0x0014: for(j=0;j//地址不在规定返回或者长度太长,返回错误 sendbuff[0] = mymodbus_add; sendbuff[1] = 0x80 | USART2_BUF[1]; sendbuff[2] = 0; crc = Checksum_CRC16(sendbuff,3); sendbuff[3] = (u8)(crc >> 8); sendbuff[4] = (u8)crc; Uart2_sendBuffer(sendbuff,5); } } void Modbus_06_ack(){ } 3.1.2 485 驱动测试

485接收部分驱动完成,这里需要测试一下,测试过程确实发现了问题,但是都是小问题,整体框架逻辑是正常的,在处理校验位的时候,数据位数处理有点大意了(上面贴出的程序是已经修改过的): 在这里插入图片描述 把第一个寄存器设置一个值: 在这里插入图片描述 测试结果,一切正常: 在这里插入图片描述

测完了还是把 06 写单个寄存器的函数给补充一下,目前只开放一个数据,就是设备ID的写入,直接上代码:

void Modbus_06_ack(){ u16 Register_add; // u16 val; // u16 crc; u8 i; u8 sendbuff[8] = {0}; if(USART2_Data mymodbus_add = val; Register_value[3] = mymodbus_add; i = 0; sendbuff[i++] = mymodbus_add; sendbuff[i++] = 0x06; sendbuff[i++] = (u8)(Register_add>>8); sendbuff[i++] = (u8)Register_add; sendbuff[i++] = (u8)(val>>8); sendbuff[i++] = (u8)val; crc = Checksum_CRC16(sendbuff,i); sendbuff[i++] = (u8)(crc >> 8); sendbuff[i++] = (u8)crc; Uart2_sendBuffer(sendbuff,i); } else{//写地址不在规定范围 sendbuff[0] = mymodbus_add; sendbuff[1] = 0x80 | USART2_BUF[1]; sendbuff[2] = 3; crc = Checksum_CRC16(sendbuff,3); sendbuff[3] = (u8)(crc >> 8); sendbuff[4] = (u8)crc; Uart2_sendBuffer(sendbuff,5); } } else{//写地址不在规定范围 sendbuff[0] = mymodbus_add; sendbuff[1] = 0x80 | USART2_BUF[1]; sendbuff[2] = 1; crc = Checksum_CRC16(sendbuff,3); sendbuff[3] = (u8)(crc >> 8); sendbuff[4] = (u8)crc; Uart2_sendBuffer(sendbuff,5); } }

测试一下效果,如下图,看上去OK: 在这里插入图片描述 想了一下,不对啊?

修改地址应该有个限制,从机的地址应该在: 1~ 247 (0XF7)

所以以代码里面还得加一个限制(按理来说应该是先判断地址,如果是写ID的寄存器,才需要限制从机地址大小,其他寄存器,是可以写其他数值的,因为示例中只开放ID寄存器的写操作,所以这里问题不大,但是在完善自己的从机代码的时候这里得注意一下逻辑!!!): 在这里插入图片描述

3.2 D6T传感器驱动(I2C协议)

I2C的基础知识这里就不多介绍了,网上很多,我那个总线协议记录里面也介绍了。

3.2.1 通用I2C驱动

I2C协议需要用到 us 延时,HAL库里面没有, FreeRTOS里面也没,这里我们还是用以前常用的自己写一个: 在这里插入图片描述

我们使用的是软件的I2C,所以要把通用的 I2C 驱动写一下,新建一个i2c.c和一个i2c.h文件作为软件 I2C的通用驱动,这里直接上源码:

#include "i2c.h" // ------------------------------------------------------------------ void i2c_init(void) { // the SDA and SCL pins are defined as input with pull up enabled // pins are initialized as inputs, ext. pull => SDA and SCL = high } // ------------------------------------------------------------------ // send start sequence (S) void i2c_start(void) { sda_high; delay_us(5); scl_high; delay_us(10); sda_low; delay_us(10); scl_low; //使SCL置低,准备发送或者接受数据 delay_us(10); } // ------------------------------------------------------------------ // send stop sequence (P) void i2c_stop(void) { sda_low; delay_us(5); scl_low; delay_us(10); scl_high; delay_us(5); sda_high; delay_us(10); } // ------------------------------------------------------------------ // returns the ACK or NACK uint8 i2c_write(uint8 u8Data) { uint8 u8Bit; uint8 u8AckBit; // write 8 data bits u8Bit = 0x80; //msb first while(u8Bit) { if(u8Data&u8Bit) { sda_high; delay_us(20); } //& compare every bit else{ sda_low; delay_us(20); } scl_high; delay_us(30); u8Bit >>= 1; //next bit scl_low; delay_us(30); } // read acknowledge (9th bit) sda_high; delay_us(10); scl_high; delay_us(10); u8AckBit= sda_read; //#define sda_read() (sda_port & sda_pin)? 1 :0 ack on bus is low -> u8AckBit = 1 sda_port gpio0 sda_pin SCSEDIO0 delay_us(10); scl_low; delay_us(10); return u8AckBit; } //读1个字节,ack=1时,发送ACK,ack=0,发送nACK u8 i2c_read_byte(unsigned char ack) { unsigned char i,receive=0; // MYSDA_IN;//SDA设置为输入 for(i=0;i uint8 u8Bit; uint8 u8Data; u8Bit = 0x80; // msb first u8Data = 0; while(u8Bit){ scl_high; delay_us(20); u8Bit >>= 1; //next bit u8Data sda_high; delay_us(20); } scl_high; delay_us(20); scl_low; delay_us(20); sda_high; delay_us(20); return u8Data; } u8 i2c_wait_ack(void) { u8 ucErrTime=0; delay_us(5); sda_high;delay_us(5); //MCU DATA 置高,外面高就是高,外面低就是低 scl_high; delay_us(5); //CLK 高电平期间数据有效 while(sda_read) //低电平为有应答,高电平无应答 { ucErrTime++; if(ucErrTime>250) { i2c_stop(); return 1; } } delay_us(10); scl_low; return 0; } void IIC_Ack(void) { scl_low; //SCL为低,SDA为低,SCL为高,SDA为低,应答低电平有效,SCL为低,产生应答信号 // MYSDA_OUT; sda_low; delay_us(10); scl_high; delay_us(10); scl_low; delay_us(10); sda_high; } void IIC_NAck(void) { scl_low; //SCL为低,SDA为高,SCL为高,SCL为低 // MYSDA_OUT; sda_high; delay_us(10); scl_high; delay_us(10); scl_low; } //IIC发送一个字节 //返回从机有无应答 //1,有应答 //0,无应答 void i2c_send_byte(u8 txd) { u8 t; // MYSDA_OUT; scl_low; //拉低时钟开始数据传输 ,SCL为低,SDA变高或者变低(数据位),SCL变高,SCL变低,期间SDA为1既1,为0既0 for(t=0;t u8 D6Tbuff[20]; u8 D6T_Data=0; // u16 tPEC; i2c_start(); i2c_send_byte(0X14); //地址,和读写指令 i2c_wait_ack(); delay_us(150); //这里必须加 i2c_send_byte(0X4C); i2c_wait_ack(); delay_us(150); i2c_start(); i2c_send_byte(0X15); //地址,读指令 i2c_wait_ack(); delay_us(120); // D6T44L_ReadLenByte(5); //D6T-1A-02 只有5个数值 u8 t; D6T_Data=0; for(t=0;t


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有