文章目录
- 前言
- 1、要求
- 2、系统设计
- 3、功能模块
- 3、系统功能模块图
- 一、stm32控制模块原理图
- 二、各功能模块的实现
- 1、整个系统的基本配置
- 2、RTOS多任务
- 1、设计线程
- 2、配置主函数代码
- 3、温湿度读取模块(I2C)
- 4、LED定时开关灯(pwm)
- 5、按键实现报警信号
- 6、脉搏&血氧数据读取
- 7、UART 转 485 接口与 modbus 通信
- 三、总结
前言
1、要求
设计一个基于物联网技术的智慧病房管理系统。假设医院住院部的一层病房(走廊两边病房平行分布),病房数量最多60间,每间病房3个床位,编号从 1~180 号。每间病房可采用的设备如下:STM32F103 开发板 1 块,房间温湿度采集模块 1 套( I2C 接口,AHT20 模块),房间自动灯光开关控制器(以 PWM 方式控制,每天早上 7 点渐亮,晚上 22 点渐灭),病人脉搏 & 血氧检测仪 3 套( UART 接口输出脉搏 + 血氧的数字值),床头紧急呼叫按键开关 3 个(按下呼叫)。
每间病房的 STM32F103 开发板通过 UART 转 485 接口,以 mobus 组网方式,连接到护士监控室的 PC 电脑上(上位机)。PC 电脑上可接收每间病房的温湿度数据(周期为 5 分钟)、床头紧急呼叫信号、病人脉搏血氧数据(正常状态下 30 分钟一次采集;当脉搏超过 120 或血氧值低于 90 时切换到危重状态下,实时采集),显示在屏幕上并且保存到 MySQL 数据库里。
2、系统设计
对于该方案,最初的想法是设置一个rtos多任务框架,然后把要求里的框架一个一个都放到线程当中进行完成,包括有AHT的温湿度读取、脉搏&血氧数据读取、按键紧急呼叫、定时开关灯等任务。
3、功能模块
1、脉搏&血氧的数据的检测:脉搏超过120或血氧低于90时,实时采集,否则30分钟采集一次。
2、按键实现紧急呼叫:一个GPIO接口对输入信号的高低电平进行判断。
3、AHT20模块读取温湿度信息:5分钟采集一次。
4、PWM控制灯光定时亮灭:早上7点渐亮,晚上10点渐暗。
5、通过UART转485实现modbus通信。
3、系统功能模块图
一、stm32控制模块原理图
STM32最小原理图:
AHT20原理图:
电路原理图:
二、各功能模块的实现
1、整个系统的基本配置
- RCC设置:
- SYS设置
- UART设置串口模式
- I2C设置DMA模式:
- TIME3:
TIME2:
NVIC:
GPIO:
时钟树设置:
移植RT-thread Nano:
- 设置时间:
2、RTOS多任务
具体移植过程参考:
移植RT-thread Nano完成一个 modbus接口的温湿度Slave设备,让上位机PC通过modbus协议获取温湿度
1、设计线程
- 在Application/USER文件夹下新建app_rt_thread.c文件
1、引入所需头文件:
#include "rtthread.h"
#include "main.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"
#include "stdio.h"
#include "AHT20.h"
这里我们设置4个子进程,主进程为串口向上位机发送数据,子进程有温湿度的获取、脉搏&血氧数据测量、按键紧急呼叫、LED的亮灭。
struct rt_thread led1_thread;
rt_uint8_t rt_led1_thread_stack[128];
void led1_task_entry(void *parameter);
struct rt_thread usart1_thread;
rt_uint8_t rt_usart1_thread_stack[128];
void usart1_task_entry(void *parameter);
struct rt_thread usart2_thread;
rt_uint8_t rt_usart2_thread_stack[128];
void usart2_task_entry(void *parameter);
struct rt_thread button_thread;
rt_uint8_t rt_button_thread_stack[128];
void button_task_entry(void *parameter);
//初始化线程函数
void MX_RT_Thread_Init(void)
{
//初始化LED1线程
rt_thread_init(&led1_thread,"led1",led1_task_entry,RT_NULL,&rt_led1_thread_stack[0],sizeof(rt_led1_thread_stack),3,20);
rt_thread_init(&usart1_thread,"usart1",usart1_task_entry,RT_NULL,&rt_usart1_thread_stack[0],sizeof(rt_usart1_thread_stack),3,20);
rt_thread_init(&usart2_thread,"usart2",usart2_task_entry,RT_NULL,&rt_usart2_thread_stack[0],sizeof(rt_usart2_thread_stack),3,20);
rt_thread_init(&button_thread,"button",button_task_entry,RT_NULL,&rt_button_thread_stack[0],sizeof(rt_button_thread_stack),3,20);
//开启线程调度
rt_thread_startup(&led1_thread);
rt_thread_startup(&usart1_thread);
rt_thread_startup(&usart2_thread);
rt_thread_startup(&button_thread);
}
//主任务
void MX_RT_Thread_Process(void)
{
printf("系统正在运行!!!");
rt_thread_delay(2000);
}
//LED1任务
void led1_task_entry(void *parameter)
{
}
//读取温湿度
void usart1_task_entry(void *parameter)
{
}
//读取脉搏&血氧
void usart2_task_entry(void *parameter)
{
}
//按键
void button_task_entry(void *parameter)
{
}
2、配置主函数代码
1、在main,c文件中添加头文件和引入相应函数;
2、初始化线程和执行主程序
3、温湿度读取模块(I2C)
具体过程:
通过STM32Cube配置完成基于I2C协议的AHT20温湿度传感器的数据采集
温湿度读取模块这里我们采用的传感器是AHT20、I2C协议。I2C 通讯协议(Inter-Integrated Circuit)是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备,现在被广泛地
使用在系统内多个集成电路(IC)间的通讯。
1、上电后等待一段时间待设备正常工作后再读取温湿度:
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
uint32_t CT_data[2]={0,0};
volatile int c1,t1;
rt_thread_delay(50);
AHT20_Init();
rt_thread_delay(2500);
2、CRC校验后读取温湿度数据,随后延时5分钟
AHT20_Read_CTdata_crc(CT_data); //crc校验后,读取AHT20的温度和湿度数据
c1 = CT_data[0]*100*10/1024/1024; //计算得到湿度值c1(放大了10倍)
t1 = CT_data[1]*200*10/1024/1024-500;//计算得到温度值t1(放大了10倍)
printf("湿度:%d%s",c1/10,"%");
printf("温度:%d%s",t1/10,"℃");
printf("\r\n");
rt_thread_mdelay(300000);//线程睡眠大约5分钟,挂起执行其他操作
4、LED定时开关灯(pwm)
这里我们使用上面配置过的time2定时器加PWM。
获取日期、时间:
RTC_DateTypeDef Data; //获取日期结构体
RTC_TimeTypeDef Time; //获取时间结构体
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);
while(1){
HAL_RTC_GetTime(&hrtc,&Time,RTC_FORMAT_BIN);
if(Time.Hours == 7&&Time.Minutes == 0&&Time.Seconds == 0){
for(uint16_t i=1;i<500;i++){//灯渐亮
htim2.Instance->CCR2 = i;
rt_thread_delay(5);
}
}
else if(Time.Hours == 22&&Time.Minutes == 0&&Time.Seconds == 0){
for(uint16_t i=499;i>=1;i--){//灯渐灭
htim2.Instance->CCR2 = i;
rt_thread_delay(5);
}
}
}
在初始化之前先获取一次实时时间作为标准:
void getRealTime(void)
{
HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &datebuff, RTC_FORMAT_BIN);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, (uint16_t)datebuff.Year);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR3, (uint16_t)datebuff.Month);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR4, (uint16_t)datebuff.Date);
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR5, (uint16_t)datebuff.WeekDay);
}
void HAL_RTCEx_RTCEventCallback(RTC_HandleTypeDef *hrtc)
{
if(hrtc->Instance == RTC)
{
getRealTime();
}
}
5、按键实现报警信号
因为前面我们随意设置了PB5引脚作为输入,即为按钮,按钮按下发出报警信号,这里我们使用PA4、PA5、PA6引脚的高低电平作为三个按键的报警信号:
按键子线程代码:
while(1){
switch(KEY_Scan(0))
{
case KEY1_PRES:
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_4);
break;
case KEY2_PRES:
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_5);
break;
case KEY3_PRES:
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_6);
break;
default:
break;
}
}
按键消抖:
uint8_t KEY_Scan(uint8_t mode)
{
static uint8_t key_up=1;//按键松开标志位
if(key_up&&((KEY1==0)||(KEY2==0)||(KEY3==0)))
{
HAL_Delay(10);//过滤抖动时间
key_up=0;
if(KEY1==0)return KEY1_PRES;
if(KEY2==0)return KEY2_PRES;
if(KEY3==0)return KEY3_PRES;
}
else if(KEY1==1&&KEY2==1&&KEY3==1)key_up=1;
return 0;//无按键按下
}
6、脉搏&血氧数据读取
注意:这里的脉搏&血氧仪没使用过,应该也是通过串口采用modbus协议进行通信:
具体的usart通信可参考:通过stm32CubeMX完成一个STM32的USART串口通讯程序
定义脉搏和血氧的地址和变量:
#define REG_SPO2_ADDRESS 0x0001
#define REG_PULSE_RATE_ADDRESS 0x0002
uint16_t spo2_value;
uint16_t pulse_rate_value;
modbus读寄存器请求:
void modbus_handle_read_registers(uint8_t *data, uint16_t len)
{
uint16_t start_address;
uint16_t register_count;
uint16_t i;
// 解析读寄存器的请求报文
start_address = (data[2] << 8) | data[3];
register_count = (data[4] << 8) | data[5];
// 根据地址和数量读取寄存器数据
for (i = 0; i < register_count; i++)
{
if (start_address + i == REG_SPO2_ADDRESS)
{
data[9 + i * 2] = (spo2_value >> 8) & 0xFF;
data[10 + i * 2] = spo2_value & 0xFF;
}
else if (start_address + i == REG_PULSE_RATE_ADDRESS)
{
data[9 + i * 2] = (pulse_rate_value >> 8) & 0xFF;
data[10 + i * 2] = pulse_rate_value & 0xFF;
}
}
// 发送读寄存器响应报文
uart_send_data(data, 9 + register_count * 2);
}
主循环函数:
void modbus_loop(void)
{
uint8_t data[256];
uint16_t len;
uint16_t new_spo2;
uint16_t new_pulse_rate;
while (1)
{
// 等待接收数据
len = uart_receive_data(data, sizeof(data));
// 处理读寄存器请求
if (data[0] == 0x01 && data[1] == 0x03)
{
modbus_handle_read_registers(data, len);
}
// 处理写寄存器请求
else if (data[0] == 0x01 && data[1] == 0x10)
{
// 根据地址和数量读取寄存器数据
new_spo2 = (data[9] << 8) | data[10];
new_pulse_rate = (data[11] << 8) | data[12];
update_spo2_and_pulse_rate(new_spo2, new_pulse_rate);
// 发送写寄存器响应报文
uart_send_data(data, 8);
}
}
}
定义采样状态:
typedef enum {
NORMAL,
CRITICAL
} SamplingState;
SamplingState sampling_state = NORMAL;
定义时间脉搏:
#define NORMAL_SAMPLING_INTERVAL 1800 // 30 分钟
#define CRITICAL_SAMPLING_INTERVAL 10 // 10 秒、算作实时
#define CRITICAL_PULSE_RATE_THRESHOLD 120//脉搏120
#define CRITICAL_SPO2_THRESHOLD 90//血氧90
回调函数实现分状态采样:
void sampling_timer_callback(void)
{
if (sampling_state == NORMAL)//正常状态
{
if (pulse_rate_value > CRITICAL_PULSE_RATE_THRESHOLD || spo2_value < CRITICAL_SPO2_THRESHOLD)
{
sampling_state = CRITICAL;
timer_start(CRITICAL_SAMPLING_INTERVAL, sampling_timer_callback);
}
else
{
// 正常状态下采样
send_request_to_spo2_pulse_sensor();
timer_start(NORMAL_SAMPLING_INTERVAL, sampling_timer_callback);
}
}
else
{
// 危重状态下采样
send_request_to_spo2_pulse_sensor();
if (pulse_rate_value <= CRITICAL_PULSE_RATE_THRESHOLD && spo2_value >= CRITICAL_SPO2_THRESHOLD)
{
sampling_state = NORMAL;
timer_start(NORMAL_SAMPLING_INTERVAL, sampling_timer_callback);
}
else
{
timer_start(CRITICAL_SAMPLING_INTERVAL, sampling_timer_callback);
}
}
}
7、UART 转 485 接口与 modbus 通信
RS-485通信使用到了串口3,这里发现我们少设置了一个串口,波特率采用9600,开启串口3的发送DMA和接收DMA,都选择普通模式。
modbus协议发送数据:
void RS485_BUS_SendData(u8 *buf, u8 len)
{
u8 t;
for(t=0;t<len;t++)
{
while(USART_GetFlagStatus(UART5,USART_FLAG_TC)==RESET);
USART_SendData(UART5,buf[t]);
}
while(USART_GetFlagStatus(UART5,USART_FLAG_TC)==RESET);
RS485_BUS_RXCNT = 0;
}
modbus协议接受数据:
void RS485_BUS_ReceiveData(u8 *buf,u8 *len)
{
u8 rxlen=RS485_BUS_RXCNT;
u8 i=0;
*len=0;
delay_ms(10);
if((rxlen==RS485_BUS_RXCNT)&&rxlen) //接收到了数据且接收完成
{
for(i=0;i<rxlen;i++)
{
buf[i]=RS485_BUS_RXBUF[i];
}
*len=RS485_BUS_RXCNT;
RS485_BUS_RXCNT=0;
}
}
三、总结
本次系统设计采用了RTthread-nano的多任务框架,一下子使得整个系统的路线清晰了起来,并且还可以设置线程的优先等级,做好整个系统的规划。同时,采用这种多线程的的并发任务处理,每个线程都有自己独立的运行空间,即线程堆栈,每个线程都专注于做自己的事情,互不干涉,独立性高。并且各个线程在使用delay函数时,线程会将任务挂起,自动让出CPU供其他需要的线程进行使用,我们可以放心的在线程里使用延时函数了同时CPU的利用率得到了极大的提高。此次系统的代码我只是根据设计的几个模块,分别划分到多任务处理框架之中,对相应的每个模块的主要代码或部分进行了一个展示,实际里面每一个模块的实现都还需要一定的时间去实现,以及会遇到各种各样的困难。虽然此次,大作业只是进行一个方案设计但内容涵盖了较多本学期的内容,在设计过程中发现有的地方印象已经没有那么深刻就又进行相应部分的复习,再回过头来看发现理解又不一样,感觉还是不错!