文章目录
- 一、硬件资源介绍与接线
- 二、创建与体验第 1 个工程
- 2.1 创建工程
- 2.2 配置调试器
- 2.3 配置 GPIO 操作 LED
- 三、UART 编程
- 3.1 使用 STM32CubeMX 进行配置
- 3.1.1 UART1
- 3.1.2 配置 RS485方向引脚
- 3.2 封装 UART
- 3.3 上机实验
- 3.3.1 硬件连接
- 3.3.2 STM32H5 程序改造
- 3.3.3 STM32F030程序调试
- 3.3.4 现象
- 四、libmodbus 移植
- 4.1 移植 libmodbus
- 4.2 使用 modbus 控制设备
- 4.3 上机实验
- 五、传感器设计
- 5.1 设计思路
- 5.2 三款传感器功能及所用引脚
- 5.3 点表设计
- 5.3.1 开关量模块(SWITCH)
- 5.3.2 环境监测模块(ENV_MONITOR)
- 5.3.3 温湿度模块(TEMP HUMI)
- 5.4 开关量传感器程序设计
- 5.5 环境监测传感器程序设计
- 5.5.1 硬件电路
- 5.5.2 配置 GPIO 和 ADC
- 5.5.3 读取 ADC 的关键代码
- 5.5.4 相关代码及现象
- 5.6 温湿度传感器程序设计
- 5.6.1 硬件电路与操作方法
- 5.6.2 配置 I2C
- 5.6.3 读取温湿度关键代码
- 5.6.4 相关代码及现象
一、硬件资源介绍与接线
Modbus 传感器开发套件共有三个, 三个板子的使用的主控方案是 STM32F030芯片,硬件接口资源如下图所示:
开关量模块
温湿度变送器模块
环境检测模块
二、创建与体验第 1 个工程
2.1 创建工程
启动 STM32CubeMX 后,点击如下图标开始选择 MCU:
如下图输入型号“STM32F030CCT”,双击找到的芯片, 开始创建工程:
调高 CPU 频率:
配置工程, 如下操作:
指定代码生成方法, 如下:
2.2 配置调试器
新建的工程要配置调试器,参考《2.2.3 配置调试器》 。
然后就可以编译程序、烧写运行了。
2.3 配置 GPIO 操作 LED
根据开发板原理图可以看到 F030的LED 引脚图如下:
可以双击打开工程中如下文件进入STM32CubeMX进行配置:
然后如下配置 PB11 、PB12 、PB13 为输出引脚:
点击右上角的 “GENERATE CODE”按钮后打开工程。
在main函数的循环里, 增加如下代码:
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
/* set LED output high */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET); //LED1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); //LED2
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); //LED3
HAL_Delay(500);
/* set LED output low */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET);//LED1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); //LED2
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET); //LED3
HAL_Delay(500);
}
最后编译、烧写、运行, 可以看到开发板的 LED 闪烁。
三、UART 编程
3.1 使用 STM32CubeMX 进行配置
RS4385接口原理图如下:
需要在STM32CubeMX里配置UART1,并且配置PA8为输出引脚。
其中RS485 CTRL引脚作用:是用来控制对应的SIT3088ETK芯片,决定了数据的走向:把数据发送出去或者接收到数据后回送给主控。当要发送数据的时候将此引脚设置为高电平(DE),接收数据时设置为低电平(RE#)
3.1.1 UART1
先使能 UART1:
然后使能中断:
在前面STM32H5的UART程序里使用了DMA,本节故意不使用DMA而使用纯中断来 实现UART,多学一种编程方法。
3.1.2 配置 RS485方向引脚
STM32H5主控板上使用的RS485转换芯片是MAX13487EESA,它会自动切换发送、 接收方向,无需程序进行方向的控制。使用STM32F030制作的“廉价传感器”里,使用 的RS485转换芯片是SIT3088ETK,它需要使用一个GPIO来控制方向,如下图所示:
上图中,RS485_CTRL使用的引脚是PA8,所以还需要把它配置为输出引脚,输出低 电平(让SIT3088ETK默认为接收状态) 。如下配置:
3.2 封装 UART
将以下路径的文件复制并粘贴到上面创建的工程uart_demoo:
这里只需要uart_driver,所以将其他文件删除,如下:
接下来用source insight编写uart_driver.c以及uart_driver.h:
uart_driver.c
#include <stdio.h>
#include <string.h>
#include "uart_device.h"
struct UART_Data {
UART_HandleTypeDef *huart;
GPIO_TypeDef* GPIOx_485; //使用哪一组引脚
uint16_t GPIO_Pin_485; //使用哪一个引脚
QueueHandle_t xRxQueue;
QueueHandle_t xTxSem;
uint8_t rxdata;
};
static struct UART_Data g_uart1_data = {
&huart1,
GPIOA,
GPIO_PIN_8,
};
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
struct UART_Data * uart_data = g_uart1_dev.priv_data; //这里只有uart1 所以直接获得其私有数据
xSemaphoreGiveFromISR(uart_data->xTxSem, NULL);
/* 配置RS485转换芯片的方向引脚,让它输出0表示接收 */
HAL_GPIO_WritePin(uart_data->GPIOx_485, uart_data->GPIO_Pin_485, GPIO_PIN_RESET); //对应PA8
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
struct UART_Data * uart_data = g_uart1_dev.priv_data;
xQueueSendFromISR(uart_data->xRxQueue, (const void *)&uart_data->rxdata, NULL);
HAL_UART_Receive_IT(uart_data->huart, &uart_data->rxdata, 1); //若接收完毕则再次启动 为了下一次接收
}
}
static int stm32_uart_init(struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit)
{
struct UART_Data * uart_data = pDev->priv_data;
if (!uart_data->xRxQueue) //如果队列为空 则创建队列和信号量
{
uart_data->xRxQueue = xQueueCreate(200, 1);
uart_data->xTxSem = xSemaphoreCreateBinary( );
/* 配置RS485转换芯片的方向引脚,让它输出0表示接收 */
HAL_GPIO_WritePin(uart_data->GPIOx_485, uart_data->GPIO_Pin_485, GPIO_PIN_RESET);
HAL_UART_Receive_IT(uart_data->huart, &uart_data->rxdata, 1); //启动数据传输
}
return 0;
}
static int stm32_uart_send(struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout)
{
struct UART_Data *uart_data = pDev->priv_data;
/* 配置RS485转换芯片的方向引脚,让它输出1表示发送 */
HAL_GPIO_WritePin(uart_data->GPIOx_485, uart_data->GPIO_Pin_485, GPIO_PIN_SET);
HAL_UART_Transmit_IT(uart_data->huart, datas, len);
/* 等待1个信号量(为何不用mutex? 因为在中断里Give mutex会出错) */
if (pdTRUE == xSemaphoreTake(uart_data->xTxSem, timeout))
HAL_GPIO_WritePin(uart_data->GPIOx_485, uart_data->GPIO_Pin_485, GPIO_PIN_RESET);
return 0;
else
HAL_GPIO_WritePin(uart_data->GPIOx_485, uart_data->GPIO_Pin_485, GPIO_PIN_RESET);
return -1;
}
static int stm32_uart_recv(struct UART_Device *pDev, uint8_t *pData, int timeout)
{
struct UART_Data *uart_data = pDev->priv_data;
if (pdPASS == xQueueReceive(uart_data->xRxQueue, pData, timeout))
return 0;
else
return -1;
}
static int stm32_uart_flush(struct UART_Device *pDev)
{
struct UART_Data *uart_data = pDev->priv_data;
int cnt = 0;
uint8_t data;
while (1)
{
if (pdPASS != xQueueReceive(uart_data->xRxQueue, &data, 0))
break;
cnt++;
}
return cnt;
}
struct UART_Device g_uart1_dev = {"uart1", stm32_uart_init, stm32_uart_send, stm32_uart_recv, stm32_uart_flush, &g_uart1_data};
static struct UART_Device *g_uart_devices[] = {&g_uart1_dev};
struct UART_Device *GetUARTDevice(char *name)
{
int i = 0;
for (i = 0; i < sizeof(g_uart_devices)/sizeof(g_uart_devices[0]); i++)
{
if (!strcmp(name, g_uart_devices[i]->name))
return g_uart_devices[i];
}
return NULL;
}
uart_driver.h
#ifndef __UART_DEVICE_H
#define __UART_DEVICE_H
#include <stdint.h>
struct UART_Device {
char *name;
int (*Init)( struct UART_Device *pDev, int baud, char parity, int data_bit, int stop_bit);
int (*Send)( struct UART_Device *pDev, uint8_t *datas, uint32_t len, int timeout);
int (*RecvByte)( struct UART_Device *pDev, uint8_t *data, int timeout);
int (*Flush)(struct UART_Device *pDev);
void *priv_data;
};
struct UART_Device *GetUARTDevice(char *name);
#endif /* __UART_DEVICE_H */
3.3 上机实验
要测试 STM32F030 的串口, 只需要把它的 485 接口连接到 PC 去就可以了,但是我们没 有 PC 上使用的“USB 转 485”模块,所以使用 STM32H5 来实现一个“USB 转 485 模块”:
-
它从 USB 串口读到数据,再从 485 接口发送出去;
-
它从 485 接口读到数据,再从 USB 串口发送给 PC。
3.3.1 硬件连接
3.3.2 STM32H5 程序改造
本节源码为“h5_demo”。
app_freertos.c
static void RS485toUSB( void *pvParameters )
{
struct UART_Device *pUSBUART = GetUARTDevice("usb");
struct UART_Device *pRS485UART = GetUARTDevice("uart4");
uint8_t data;
while (1)
{
if (0 == pRS485UART->RecvByte(pRS485UART, &data, portMAX_DELAY))
{
pUSBUART->Send(pUSBUART, &data, 1, 100);
}
}
}
static void USBtoRS485( void *pvParameters )
{
struct UART_Device *pUSBUART = GetUARTDevice("usb");
struct UART_Device *pRS485UART = GetUARTDevice("uart4");
uint8_t data;
while (1)
{
if (0 == pUSBUART->RecvByte(pUSBUART, &data, portMAX_DELAY))
{
pRS485UART->Send(pRS485UART, &data, 1, 100);
}
}
}
struct UART_Device *pUSBUART = GetUARTDevice("usb");
struct UART_Device *pRS485UART = GetUARTDevice("uart4");
pUSBUART->Init(pUSBUART, 115200, 'N', 8, 1); //初始化:设备ID、波特率、检验位、数据位、停止位
pRS485UART->Init(pRS485UART, 115200, 'N', 8, 1);
3.3.3 STM32F030程序调试
本节源码为“f030_demo”。
freertos.c
#include "uart_device.h"
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
struct UART_Device *pdev = GetUARTDevice("uart1");
pdev->Init(pdev, 115200, 'N', 8, 1);
uint8_t data;
int levels[3] = {0};
while (1)
{
if (0 == pdev->RecvByte(pdev, &data, portMAX_DELAY)) //读取到来自h5的数据
{
pdev->Send(pdev, &data, 1, 100); //将其回送给h5
switch (data)
{
case 1:
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, levels[0]); //LED1
levels[0] = !levels[0]; //每次发送完毕取反 下次发送就会使得现象与上次相反 即led熄灭或亮
break;
}
case 2:
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, levels[1]); //LED1
levels[1] = !levels[1];
break;
}
case 3:
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, levels[2]); //LED1
levels[2] = !levels[2];
break;
}
}
}
}
/* USER CODE END StartDefaultTask */
}
3.3.4 现象
刚烧录好时三个LED全亮,如下:
分别输入01、02、03去控制LED熄灭(第一次输入开始赋值,取反后熄灭):
四、libmodbus 移植
本节源码为“E:\QuanCJ\libmodbus_to_f030\f030_demo”。
4.1 移植 libmodbus
把“7-6_STM32F030串口测试\h5_demo\demo\Middlewares\Third_Party\libmodbus”整 个目录复制到f030_demo中。
在Keil工程中添加代码, 如下:
4.2 使用 modbus 控制设备
STM32F030作为从设备(sever),编写“Core\Src\freertos.c”:
#include "uart_device.h"
#include "modbus.h"
#include "errno.h"
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
uint8_t *query;
modbus_t *ctx;
int rc;
modbus_mapping_t *mb_mapping;
ctx = modbus_new_st_rtu("uart1", 115200, 'N', 8, 1);
modbus_set_slave(ctx, 1);
query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);
mb_mapping = modbus_mapping_new_start_address(0,
10,
0,
10,
0,
10,
0,
10);
memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);
memset(mb_mapping->tab_registers, 0x55, mb_mapping->nb_registers*2);
rc = modbus_connect(ctx);//连接
if (rc == -1) {
//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
modbus_free(ctx);
vTaskDelete(NULL);;
}
for (;;) {
do {
rc = modbus_receive(ctx, query);//接收主机发送过来的modbus请求
/* Filtered queries return 0 */
} while (rc == 0);
/* The connection is not closed on errors which require on reply such as
bad CRC in RTU. */
if (rc == -1 && errno != EMBBADCRC) {
/* Quit */
continue;
}
rc = modbus_reply(ctx, query, rc, mb_mapping);//回复主机
if (rc == -1) {
//break;
}
if (mb_mapping->tab_bits[0])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); //LED1:可读可写的第0位为1则led1亮
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
if (mb_mapping->tab_bits[1])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); //LED2:可读可写的第1位为1则led2亮
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
if (mb_mapping->tab_bits[2])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET); //LED3:可读可写的第2位为1则led3亮
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
}
modbus_mapping_free(mb_mapping);
vPortFree(query);
/* For RTU */
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
/* USER CODE END StartDefaultTask */
}
4.3 上机实验
这里分别三次输入0、1、2并设置成on,则三个LED都亮。
五、传感器设计
5.1 设计思路
上位机(PC 软件)或中控(STM32H5)通过 modbus 协议访问 STM32F030 传感器时,读写的是 STM32F030 分配出来的 4 个类型的缓冲区。这里需要解决 2 个问题:
-
这 4 个类型的
缓冲区起始地址、大小
分别是多少? 这根据传感器的功能来设置。比如有 2 个按键,那么就可以分配 2 个“只读的位寄存器”(DI)。 -
这些寄存器的值, 如何跟硬件对应? 比如上位机读 DI 寄存器时, 谁提供这些值?传感器的程序应该读取按键值,填充 DI 寄存器。
第 1 个步骤,被称为“点表设计”。
①、主设备是主控,从设备就是传感器,我们在设置这些传感器时,首先需要确定其设备地址,因为每个传感器的设备地址不同
。
②、我们使用 libmodbus 来传输数据时,对离散输入状态会分配一个 buf ,对线圈状态也会分配一个 buf ,对下面的多位值寄存器输入寄存器和保持寄存器也会分配一个 buf ,这个 buf 会包含起始地址(指定四个寄存器其中一个的寄存器起始地址)、个数和功能
。个数需要根据硬件功能来确定;起始地址由自己决定,但一般都是从0开始;这些寄存器的功能是什么,换句话说我们读取到寄存器的值有什么含义,他的值来自哪里,所以功能分为两部分:值的含义、读取数值的来源以及写入数值如何操作硬件,我们可以以开关量寄存器为例,当我们读 DI 寄存器时,确实可以读取到数据,读取到 1 或 0 表示什么意思,此时就需要我们来决定,比如读取到 1 表示按键被按下。拿前面的几个传感器做解释如下:
5.2 三款传感器功能及所用引脚
这 3 款传感器的原理图在网盘如下目录里:
它们控制外设所用的引脚,列表如下:
功能 | 使用的引脚 | 描述 | 寄存器类别 | |
---|---|---|---|---|
ENV_MONITOR 环境监测模块 | BEEP1 | PB15 | 高电平发声 | DO |
BEEP2 | PB14 | 高电平发声 | DO | |
LED1 | PB11 | 低电平发光 | DO | |
LED2 | PB12 | 低电平发光 | DO | |
LED3 | PB13 | 低电平发光 | DO | |
OPTO_ADC | PA1 | 电压值跟光强成反比 | AI | |
RES_ADC | PA2 | 电压值跟可调电阻成反比 | AI | |
SWITCH开关量模块 | KEY1 | PA3 | 低电平表示被按下 | DI |
KEY2 | PA4 | 低电平表示被按下 | DI | |
KEY3 | PA5 | 低电平表示被按下 | DI | |
K1_CTRL | PB5 | 高电平使能继电器 | DO | |
K2_CTRL | PB4 | 高电平使能继电器 | DO | |
LED1 | PB11 | 低电平发光 | DO | |
LED2 | PB12 | 低电平发光 | DO | |
LED3 | PB13 | 低电平发光 | DO | |
TEMP_HUMI温湿度模块 | BEEP1 | PB15 | 高电平发声 | DO |
BEEP2 | PB14 | 高电平发声 | DO | |
LED1 | PB11 | 低电平发光 | DO | |
LED2 | PB12 | 低电平发光 | DO | |
LED3 | PB13 | 低电平发光 | DO | |
I2C1_SCL | PB6 | 可以读到温度、湿度 AI | AI | |
I2C2_SDA | PB7 | 可以读到温度、湿度 AI | AI |
5.3 点表设计
所谓点表, 就是一个 modbus 设备,它的地址是什么? 它里面 4 类寄存器的地址、功能是什么
。
在查看点表时,经常碰到“遥测、遥信、遥控、遥调”的概念。它们实质上就是前面讲解 modbus 时引入的“AI、DI、DO、AO”。这些概念起源于电力系统。
电力领域中四遥系统是指遥测、遥信、遥控、遥调功能系统,四遥功能是电力监控系统最基本最重要的功能。具体来说:
-
遥测(遥测信息,AI):远程测量;远方测量显示诸如电流、电压、功率、压力、温度等模拟量;
-
遥信(遥信信息,DI):远程信号;远方监视各类电气开关和设备、机械设备的工作状态和运转情况状态等;
-
遥控(遥控信息,DO):远程控制;接受并执行遥控命令,远方控制或保护电气设备及电气机械化的分合起停等工作状态;
-
遥调(遥调信息,AO):远程调节;接受并执行遥调命令,远方设定及调整所控设备的工作参数、标准参数;四遥遥测、遥信、遥控、遥调常常被简称为 AI、DI、DO、AO。
AI、DI、DO、AO 都是英文名称的首字母缩写,A 的英文全称 Analog (模拟量)、D 的英文全称 Digital (数字量) 、I 的英文全称 Input (输入)、O 的英文全称 Output (输出)。 因此,AI 表示的是模拟信号输出,AO 是模拟信号输入,DI 是数字信号输入,DO 是数字信号输出
。
随着技术不断地进步,现在也有五遥的说法, 即在四遥的基础上加上遥视, 遥视指的是指利用包括电子技术、计算机技术、自动化技术等监视并记录设备运行情况和环境安全 情况。因此伴随着技术发展,电力系统中从一遥(遥信 DI)阶段、发展到二遥(遥信 AI、 遥测 DI)、三遥(遥信 AI、遥测 DI 和遥控 DO)、四遥(遥信 AI、遥测 DI、遥控 DO 和遥调 AO);现在开始四遥向五遥过渡。
原文链接:https://blog.csdn.net/LuohenYJ/article/details/106027626
在阅读点表时,还会碰到下表中的“PLC/组态地址”,或者表中的简称“0x、1x、4x、 3x”,它们的本质都是用来分辨“AI、AO、DI、DO”四类寄存器(需要注意:在 PLC 地址中,第一位是用来分辨寄存器,而后面的才是真正的地址,都是从 1 开始,而在 modbus 中,寄存器地址从 0 开始,这里有个转换关系):
寄存器种类 | PLC/组态地址 | Modbus 寄存器地址范围 | 简称 | 读写状态 |
---|---|---|---|---|
线圈状态 | 00001~09999 | 0000H~FFFFH | 0x | 可读可写 |
离散输入状态 | 10001~19999 | 0000H~FFFFH | 1x | 只读 |
保持寄存器 | 40001~49999 | 0000H~FFFFH | 4x | 可读可写 |
输入寄存器 | 30001~39999 | 0000H~FFFFH | 3x | 只读 |
点表的设计,是完全由开发人员自行定义的。
5.3.1 开关量模块(SWITCH)
寄存器说明:
5.3.2 环境监测模块(ENV_MONITOR)
寄存器说明:
5.3.3 温湿度模块(TEMP HUMI)
寄存器说明:
5.4 开关量传感器程序设计
继电器原理图如下:
继电器对外的信号有 3 个:
- COM:公共端,通常是中间的触点, 与常开或常闭触点相连
- NC(Normally Closed): 常闭接口,继电器吸合前与 COM 连接, 吸合后悬空
- NO(Normally Open): 常开接口, 继电器吸合前悬空, 吸合后与 COM 连接
开路即通路、断路,闭合指的是开关闭合,也就是说,在没有任何上电之类的动作时,NC 和 COM 端相当于已经连通。
本节源码为“ E:\QuanCJ\switch_project\f030_demo”。
/* 这里用宏来替代是为了方便后面的其他传感器 */
#include "uart_device.h"
#include "modbus.h"
#include "errno.h"
#define USE_SWITCH_SENSOR 1 //1表示这里使用的是开关量传感器 则在ifdef中的定义及相关代码都会生效
#ifdef USE_SWITCH_SENSOR
#define SLAVE_ADDR 1 //开关量传感器的地址是01H
#define NB_BITS 5 //可读可写的位寄存器(DO)有5个
#define NB_INPUT_BITS 3 //只读的位寄存器(DI)有3个
#define NB_REGISTERS 0 //可读可写的多位寄存器(DO)有0个
#define NB_INPUT_REGISTERS 0 //只读的多位寄存器(AI)有0个
#endif
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
uint8_t *query;
modbus_t *ctx;
int rc;
modbus_mapping_t *mb_mapping;
GPIO_PinState val;
ctx = modbus_new_st_rtu("uart1", 115200, 'N', 8, 1);
modbus_set_slave(ctx, SLAVE_ADDR); //第二个参数是设备地址 用上面定义的宏表示开关量传感器的地址
query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);
mb_mapping = modbus_mapping_new_start_address(0, //分别是四类寄存器的起始地址和个数 起始地址一般为0 个数通过我们电表设计并将宏定义在前面
NB_BITS,
0,
NB_INPUT_BITS,
0,
NB_REGISTERS,
0,
NB_INPUT_REGISTERS);
rc = modbus_connect(ctx);
if (rc == -1) {
//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
modbus_free(ctx);
vTaskDelete(NULL);;
}
for (;;) {
do {
rc = modbus_receive(ctx, query);
/* Filtered queries return 0 */
} while (rc == 0);
/* The connection is not closed on errors which require on reply such as
bad CRC in RTU. */
if (rc == -1 && errno != EMBBADCRC) {
/* Quit */
continue;
}
/* 更新寄存器的值
* 1.读取GPIO
* 2.更新寄存器
*/
#ifdef USE_SWITCH_SENSOR
/* KEY1 */
val = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3); //读取按键1的值
if (val == GPIO_PIN_RESET) //如果按键1被按下
{
mb_mapping->tab_input_bits[0] = 1; //设置按键1对应DI寄存器的值 1表示被按下 0表示未被按下
}
else
{
mb_mapping->tab_input_bits[0] = 0;
}
/* KEY2 */
val = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4); //读取按键2的值
if (val == GPIO_PIN_RESET) //如果按键2被按下
{
mb_mapping->tab_input_bits[1] = 1; //设置按键2对应DI寄存器的值 1表示被按下 0表示未被按下
}
else
{
mb_mapping->tab_input_bits[1] = 0;
}
/* KEY3 */
val = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5); //读取按键3的值
if (val == GPIO_PIN_RESET) //如果按键3被按下
{
mb_mapping->tab_input_bits[2] = 1; //设置按键3对应DI寄存器的值 1表示被按下 0表示未被按下
}
else
{
mb_mapping->tab_input_bits[2] = 0;
}
#endif
rc = modbus_reply(ctx, query, rc, mb_mapping); //将寄存器的数值返回 所以前面部分代码是对寄存器进行更新
if (rc == -1) {
//break;
}
#ifdef USE_SWITCH_SENSOR
/* stitch1 */
if (mb_mapping->tab_bits[0])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET); //stitch1(高电平使能继电器)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
/* stitch2 */
if (mb_mapping->tab_bits[1])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, GPIO_PIN_SET); //stitch2(高电平使能继电器)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, GPIO_PIN_RESET);
/* LED1 */
if (mb_mapping->tab_bits[2])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); //LED1(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
/* LED2 */
if (mb_mapping->tab_bits[3])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); //LED1(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
/* LED3 */
if (mb_mapping->tab_bits[4])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET); //LED3(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
#endif
}
modbus_mapping_free(mb_mapping);
vPortFree(query);
/* For RTU */
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
/* USER CODE END StartDefaultTask */
}
现象:打开 Modbus Poll 连接上H5开发板作 usb 串行设备,并进行相应配置,将1或0输入LED和继电器对应的寄存器,会发现对应的灯发生亮灭,同时按下任意按键发现寄存器值发生变化:
两个继电器及三个LED:
三个按键:
5.5 环境监测传感器程序设计
5.5.1 硬件电路
光敏电路如下, 光照越强,U6 阻值越低,OPTO_ADC 电压值就越低:
可调电阻器如下,R33 阻值越大,RES_ADC 电压值越小:
5.5.2 配置 GPIO 和 ADC
先在STM32CubmeMX里配置GPIO和ADC 引脚,使能“Discontinuous Conversion Mode ”:
5.5.3 读取 ADC 的关键代码
// 1. 检验
HAL_ADCEx_Calibration_Start(&hadc) ;
// 启动、读2次数值
for (int i = 0; i < 2; i++)
{
HAL_ADC_Start(&hadc);
if (HAL_OK == HAL_ADC_PollForConversion(&hadc, 100))
{
mb_mapping->tab_input_registers[i] = HAL_ADC_GetValue(&hadc);
}
}
5.5.4 相关代码及现象
本节源码为“E:\QuanCJ\environment_project\f030_demo”。
#include "uart_device.h"
#include "modbus.h"
#include "errno.h"
#define USE_ENV_MONITOR_SENSOR 1
#ifdef USE_ENV_MONITOR_SENSOR //如果使用环境检测传感器
extern ADC_HandleTypeDef hadc;
#define SLAVE_ADDR 2 //环境检测传感器的地址是02H
#define NB_BITS 5 //可读可写的位寄存器(DO)有5个
#define NB_INPUT_BITS 0 //只读的位寄存器(DI)有0个
#define NB_REGISTERS 0 //可读可写的多位寄存器(DO)有0个
#define NB_INPUT_REGISTERS 2 //只读的多位寄存器(AI)有2个
#endif
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
uint8_t *query;
modbus_t *ctx;
int rc;
modbus_mapping_t *mb_mapping;
GPIO_PinState val;
#ifdef USE_ENV_MONITOR_SENSOR //如果使用环境检测传感器
HAL_ADCEx_Calibration_Start(&hadc); //进行ADC校验
#endif
ctx = modbus_new_st_rtu("uart1", 115200, 'N', 8, 1);
modbus_set_slave(ctx, SLAVE_ADDR); //第二个参数是设备地址 用上面定义的宏表示开关量传感器的地址
query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);
mb_mapping = modbus_mapping_new_start_address(0, //分别是四类寄存器的起始地址和个数 起始地址一般为0 个数通过我们电表设计并将宏定义在前面
NB_BITS,
0,
NB_INPUT_BITS,
0,
NB_REGISTERS,
0,
NB_INPUT_REGISTERS);
rc = modbus_connect(ctx);
if (rc == -1) {
//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
modbus_free(ctx);
vTaskDelete(NULL);;
}
for (;;) {
do {
rc = modbus_receive(ctx, query);
/* Filtered queries return 0 */
} while (rc == 0);
/* The connection is not closed on errors which require on reply such as
bad CRC in RTU. */
if (rc == -1 && errno != EMBBADCRC) {
/* Quit */
continue;
}
/* 更新寄存器的值(开关量传感器)
* 1.读取GPIO
* 2.更新寄存器
*/
#ifdef USE_ENV_MONITOR_SENSOR //如果使用环境检测传感器
/* 读取ADC值并更新到对应的多位寄存器 */
for (int i = 0; i < 2; i++)
{
HAL_ADC_Start(&hadc);
if (HAL_OK == HAL_ADC_PollForConversion(&hadc, 100))
{
mb_mapping->tab_input_registers[i] = HAL_ADC_GetValue(&hadc); //得到ADC的值赋给对应的多位AI寄存器
}
}
#endif
rc = modbus_reply(ctx, query, rc, mb_mapping); //将寄存器的数值返回 所以前面部分代码是对寄存器进行更新
if (rc == -1) {
//break;
}
#ifdef USE_ENV_MONITOR_SENSOR //如果使用环境监测量传感器
/* beep1 */
if (mb_mapping->tab_bits[0])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET); //beep1(高电平发声)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET);
/* beep2 */
if (mb_mapping->tab_bits[1])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET); //beep2(高电平发声)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
/* LED1 */
if (mb_mapping->tab_bits[2])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); //LED1(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
/* LED2 */
if (mb_mapping->tab_bits[3])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); //LED1(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
/* LED3 */
if (mb_mapping->tab_bits[4])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET); //LED3(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
#endif
}
modbus_mapping_free(mb_mapping);
vPortFree(query);
/* For RTU */
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
/* USER CODE END StartDefaultTask */
}
现象:打开 Modbus Poll 连接上H5开发板作 usb 串行设备,并进行相应配置,将1或0输入LED和蜂鸣器对应的寄存器,会发现对应的灯发生亮灭或者蜂鸣器响,同时光敏电阻和可调电阻对应寄存器值发生变化:
两个蜂鸣器和三个LED:
光敏电阻和可调电阻:
5.6 温湿度传感器程序设计
本节源码为“ E:\QuanCJ\TEMP_HUMI_project\f030_demo”。
5.6.1 硬件电路与操作方法
原理图如下:
AHT20 芯片资料在网盘如下目录:
AHT20 操作方法如下:
详解如下:
-
发送测量命令:传感器的 VDD 上电后需
等待 5ms
,发送写测量命令 0x70 0xAC 0x330x00
, 等待80ms
测量完成; -
获取温湿度校准数据: 在等待 80ms 测量完成后, 发送
0x71 读传感器
,可获取状态字 Status、温湿度校准数据 SRH[19:0]、ST[19:0]以及校准字 CRC
;(检查数据正确性:算出在 0x71 和 CRC 中间的 6 组数据的校验码,与得到的 CRC 进行对比,一致说明数据无误) -
根据公式计算温湿度:
计算检验码的函数如下:
//**********************************************************//
//CRC校验类型: CRC8
//多项式: X8+X5+X4+1
//Poly:0011 0001 0x31
unsigned char Calc_CRC8(unsigned char *message,unsigned char Num)
{
unsigned char i;
unsigned char byte;
unsigned char crc =0xFF;
for (byte = 0;byte<Num;byte++)
{
crc^=(message[byte]);
for(i=8;i>0;--i)
{
if(crc&0x80)
crc=(crc<<1)^0x31;
else
crc=(crc<<1);
}
}
return crc;
}//
**********************************************************//
5.6.2 配置 I2C
5.6.3 读取温湿度关键代码
·
读取一次温湿度值, 耗时至少 80ms。不可能在接收到modbus 请求后再去读温湿度。而是使用另一个任务不断读取温湿度
。
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout) ;
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout) ;
5.6.4 相关代码及现象
本节源码为“E:\QuanCJ\TEMP_HUMI_project\f030_demo”。
#include "uart_device.h"
#include "modbus.h"
#include "errno.h"
#define USE_TEMP_HUMI_SENSOR 1
#ifdef USE_TEMP_HUMI_SENSOR //如果使用温湿度传感器
extern void AHT20Task(void *argument);
static void art20_get_datas(uint16_t *temp, uint16_t *humi);
#define SLAVE_ADDR 3 //温湿度传感器的地址是03H
#define NB_BITS 5 //可读可写的位寄存器(DO)有5个
#define NB_INPUT_BITS 0 //只读的位寄存器(DI)有0个
#define NB_REGISTERS 0 //可读可写的多位寄存器(DO)有0个
#define NB_INPUT_REGISTERS 2 //只读的多位寄存器(AI)有2个
#endif
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
#ifdef USE_TEMP_HUMI_SENSOR
osThreadNew(AHT20Task, NULL, &defaultTask_attributes);
#endif
/* USER CODE BEGIN Header_StartDefaultTask */
#ifdef USE_TEMP_HUMI_SENSOR
//**********************************************************//
//CRC校验类型: CRC8
//多项式: X8+X5+X4+1
//Poly:0011 0001 0x31
unsigned char Calc_CRC8(unsigned char *message,unsigned char Num)
{
unsigned char i;
unsigned char byte;
unsigned char crc =0xFF;
for (byte = 0;byte<Num;byte++)
{
crc^=(message[byte]);
for(i=8;i>0;--i)
{
if(crc&0x80)
crc=(crc<<1)^0x31;
else
crc=(crc<<1);
}
}
return crc;
}//
static uint32_t g_temp, g_humi;//温度湿度都是20位数据
static void art20_get_datas(uint16_t *temp, uint16_t *humi)
{
*temp = g_temp;
*humi = g_humi;
}
void AHT20Task(void *argument)
{
uint8_t cmd[] = {0xAC, 0x33, 0x00}; //测试指令(这里需要注意:0x70也是测试指令 但该指令也是I2C要发送的起始设备地址 所以直接将0x70做起始设备地址并发送即可)
uint8_t datas[7];
uint8_t crc;
extern I2C_HandleTypeDef hi2c1;
vTaskDelay(5); //上电后等待5ms
while (1)
{
if (HAL_OK == HAL_I2C_Master_Transmit(&hi2c1, 0x70, cmd, 3, 100))
{
vTaskDelay(80); //发送完测试指令延时80ms
if (HAL_OK == HAL_I2C_Master_Receive(&hi2c1, 0x70, datas, 7, 100)) //前面已经发送测试指令 所以发送成功会以0x70为设备地址继续发送0x71 所以这里第二个参数只要是0x70就可以接收到后续的数据及校验码
{
/* 计算校验码 */
crc = Calc_CRC8(datas, 6); //datas数组中的前6个数组生成校验码与原来的校验码进行对比
if (crc == datas[6]) //校验码一致 数据准确
{
/* 取出湿度温度数据 */
g_humi = ((uint32_t)datas[1] << 12) | ((uint32_t)datas[2] << 4) | ((uint32_t)datas[3] >> 4);
//需要注意数据的定义 datas是8位 而g_humi是32位 要想取到正确的20位数据 需要将数据强转成32位数据并进行移位 原先的8位数据强转32位后数据只有最后的8位数据有效 其他位全为0 所以只需要将得到的数据按照20位依次位移即可 根据数据表格 湿度从data[1]开始 所以取出data[1]左移12位 则右边有12位有效数据 再取出data[2]左移4位 则右边有4位有效数据 最后data[3]中前一半数据为湿度 所以取出data[3]右移4位 相当于获得data[3]的前四位数据 刚好补上4位有效数据 这样子加起来就是20位数据
g_temp = (((uint32_t)datas[3] & 0x0f) << 16) | ((uint32_t)datas[4] << 8) | ((uint32_t)datas[5]);
//这里的取值也与湿度类似 data[3]后半部分是温度数据 所以第一部分计算的含义data[3]的后4位有效 然后就依次左移 直到取出完整的20位数据
//这样计算是因为计算出来的数值有小数 而我们定义的数值是整数 所以直接将单位缩小10倍 数值增大10倍
g_humi = g_humi * 100 * 10/ 0x100000; /* 0.1% */
g_temp = g_temp * 200 * 10/ 0x100000 - 500; /* 0.1C */
}
}
}
vTaskDelay(20); //每隔20ms获取一次数据
}
}
#endif
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
uint8_t *query;
modbus_t *ctx;
int rc;
modbus_mapping_t *mb_mapping;
GPIO_PinState val;
#ifdef USE_ENV_MONITOR_SENSOR //如果使用环境检测传感器
HAL_ADCEx_Calibration_Start(&hadc); //进行ADC校验
#endif
ctx = modbus_new_st_rtu("uart1", 115200, 'N', 8, 1);
modbus_set_slave(ctx, SLAVE_ADDR); //第二个参数是设备地址 用上面定义的宏表示开关量传感器的地址
query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);
mb_mapping = modbus_mapping_new_start_address(0, //分别是四类寄存器的起始地址和个数 起始地址一般为0 个数通过我们电表设计并将宏定义在前面
NB_BITS,
0,
NB_INPUT_BITS,
0,
NB_REGISTERS,
0,
NB_INPUT_REGISTERS);
rc = modbus_connect(ctx);
if (rc == -1) {
//fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
modbus_free(ctx);
vTaskDelete(NULL);;
}
for (;;) {
do {
rc = modbus_receive(ctx, query);
/* Filtered queries return 0 */
} while (rc == 0);
/* The connection is not closed on errors which require on reply such as
bad CRC in RTU. */
if (rc == -1 && errno != EMBBADCRC) {
/* Quit */
continue;
}
#ifdef USE_TEMP_HUMI_SENSOR //如果使用温湿度传感器
/* 这里不能让aht20发起I2C操作去捕捉数据,因为80ms太长了 */
uint16_t temp,humi;
art20_get_datas(&temp, &humi); //用此函数获得温湿度数据
mb_mapping->tab_input_registers[0] = temp; //将温度数据上传到对应的传感器
mb_mapping->tab_input_registers[1] = humi; //将湿度数据上传到对应的传感器
#endif
rc = modbus_reply(ctx, query, rc, mb_mapping); //将寄存器的数值返回 所以前面部分代码是对寄存器进行更新
if (rc == -1) {
//break;
}
#ifdef USE_TEMP_HUMI_SENSOR //如果使用温湿度传感器
/* beep1 */
if (mb_mapping->tab_bits[0])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET); //beep1(高电平发声)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET);
/* beep2 */
if (mb_mapping->tab_bits[1])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET); //beep2(高电平发声)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
/* LED1 */
if (mb_mapping->tab_bits[2])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); //LED1(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
/* LED2 */
if (mb_mapping->tab_bits[3])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); //LED1(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
/* LED3 */
if (mb_mapping->tab_bits[4])
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET); //LED3(低电平发光)
else
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
#endif
}
modbus_mapping_free(mb_mapping);
vPortFree(query);
/* For RTU */
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
/* USER CODE END StartDefaultTask */
}
现象:打开 Modbus Poll 连接上H5开发板作 usb 串行设备,并进行相应配置,将1或0输入LED和蜂鸣器对应的寄存器,会发现对应的灯发生亮灭或者蜂鸣器响,同时温度和湿度对应寄存器值发生变化:
两个蜂鸣器和三个LED:
温度和湿度: