还是一个FreeRTOS的例子,这次不是裸机工程转的,没有大部分复制的代码,所以会把步骤会记录详细一点,这应该也是博文中 FreeRTOS 最后一个例子了
平台: STM32L051C8T6
欧姆龙 D6T 红外测温传感器 I2C 协议
设备作为485从机
目录
本产品的功能就是通过红外测温传感器定时测量温度保存,设备通过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
任务中:
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]
是可以一一对应的,这个没问题。
寄存器地址 | 数组 |
---|---|
0000H | test[0] |
0001H | test[1] |
0002H | test[2] |
… | … |
00XY | test[XY] |
但是看各厂家有些设备,地址不是连续的,而且有小的有大的,比如:
上面这种情况总不能定义一个这么大的数组 test[0x0101]
(后续补充说明后来想想这个好像也不大,257,作为一款产品,肯定是厂家会保证产品地址在一定的范围内,然后可以用一个数组顺序对应寄存器地址,即便有多个通道,也是会用不同的数组表示,在一定的范围内可以离散,但是差距太大的话就得分不同数组)
当然,即使这样,我们依旧可以按照如下对应:
寄存器地址 | 数组 |
---|---|
0000H | test[0] |
0001H | test[1] |
0006H | test[2] |
0100H | test[3] |
0101H | test[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);
/*No matter the high bits before or the low bits before*/
receivecrc1 = (USART2_BUF[USART2_Data -1]<<8) + USART2_BUF[USART2_Data];
receivecrc2 = (USART2_BUF[USART2_Data]<<8) + USART2_BUF[USART2_Data - 1];
// if((lrc == receivelrc2)||(lrc == receivelrc1)){
// if(USART2_BUF[0] == mymodbus_add){
//这里说明一下,先判断地址,然后返回错误,如果先判断校验,如果出错了,那么总线上所有都同时返回就有问题了
if(USART2_BUF[0] == mymodbus_add){
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]<<8) + USART2_BUF[3]; //get add;
Register_len = (USART2_BUF[4]<<8) + USART2_BUF[5]; //get len;
u8 sendbuff[Register_len*2 + 5];
/*
如果读取的地址写错了,或者读取长度超过规定的长度
返回错误
*/
if(( 0x0010 <= Register_add)&&( Register_add <= 0x0014 )&&(Register_len < 6)){
i = 0;
sendbuff[i++] = mymodbus_add;
sendbuff[i++] = 0x03;
sendbuff[i++] = Register_len<<1;
switch(mymodbus_add){
case 0x0010:
for(j=0;j<Register_len;j++){
sendbuff[i++]= (u8)(Register_value[0+j]>>8); //发送读取数据字节数的高位
sendbuff[i++]= (u8)Register_value[0+j]; //发送读取数据字节数的低位
}
break;
case 0x0011:
for(j=0;j<Register_len;j++){
sendbuff[i++]= (u8)(Register_value[1+j]>>8);
sendbuff[i++]= (u8)Register_value[1+j];
}
break;
case 0x0012:
for(j=0;j<Register_len;j++){
sendbuff[i++]= (u8)(Register_value[2+j]>>8);
sendbuff[i++]= (u8)Register_value[2+j];
}
break;
case 0x0013:
for(j=0;j<Register_len;j++){
sendbuff[i++]= (u8)(Register_value[3+j]>>8);
sendbuff[i++]= (u8)Register_value[3+j];
}
break;
case 0x0014:
for(j=0;j<Register_len;j++){
sendbuff[i++]= (u8)(Register_value[4+j]>>8);
sendbuff[i++]= (u8)Register_value[4+j];
}
break;
default:break;
}
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] = 0;
crc = Checksum_CRC16(sendbuff,3);
sendbuff[3] = (u8)(crc >> 8);
sendbuff[4] = (u8)crc;
Uart2_sendBuffer(sendbuff,5);
}
}
void Modbus_06_ack(){
}