3.27_433:添加UART2调试打印、IO监控、指令解析和继电器控制模块。
能够接收UART2指令控制继电器开关,或向UART2发送四路IO输入状态,并使用轮询方式检测IO状态进行及时反馈。
This commit is contained in:
994
docs/application_development_plan.md
Normal file
994
docs/application_development_plan.md
Normal file
@ -0,0 +1,994 @@
|
||||
# E32-433TBH-SC 应用层软件开发计划
|
||||
|
||||
## 项目概述
|
||||
|
||||
| 项目属性 | 描述 |
|
||||
| --------- | -------------------------------- |
|
||||
| **目标MCU** | STM32F103C8T6 (Cortex-M3, 72MHz) |
|
||||
| **开发环境** | Keil MDK-ARM V5.32 |
|
||||
| **架构模式** | 裸机轮询 + 中断驱动 |
|
||||
| **当前状态** | HAL层就绪,基础通信验证通过 |
|
||||
|
||||
***
|
||||
|
||||
## 一、整体架构设计
|
||||
|
||||
### 1.1 硬件资源映射
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 硬件连接关系 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ UART1 (PA9/PA10) ◄────► RF433无线模块 (9600bps) │
|
||||
│ │
|
||||
│ UART2 (PA2/PA3) ◄────► 调试串口/上位机 (115200bps) │
|
||||
│ │
|
||||
│ UART3 (PB10/PB11) ◄────► 预留串口 (9600bps) │
|
||||
│ │
|
||||
│ MCU_DI1 (PB4) ◄────► 数字输入1 │
|
||||
│ MCU_DI2 (PB5) ◄────► 数字输入2 │
|
||||
│ MCU_DI3 (PB6) ◄────► 数字输入3 │
|
||||
│ MCU_DI4 (PB7) ◄────► 数字输入4 │
|
||||
│ │
|
||||
│ RL_Control (PA15) ◄────► 继电器控制输出 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 软件分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 应用层 (Application) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
|
||||
│ │ UART2_Print │ │ 指令解析器 │ │ IO状态监控 │ │ 继电器控制 │ │
|
||||
│ │ (调试输出) │ │ (ASCII指令) │ │ (DI1-4) │ │ (RL_Control)│ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 服务层 (Service) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ RingBuffer │ │ CmdParser │ │ Debounce │ │
|
||||
│ │ (环形缓冲区) │ │ (指令解析) │ │ (软件去抖) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 驱动层 (Driver) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ UART Driver │ │ GPIO Driver │ │ RF433 Driver │ │
|
||||
│ │ (usart.c) │ │ (gpio.c) │ │ (rf433.c) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 硬件抽象层 (HAL - STM32) │
|
||||
│ STM32F1xx_HAL_Driver / CMSIS │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 核心模块交互关系
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ UART2_Print │
|
||||
│ (调试输出) │
|
||||
└────────▲────────┘
|
||||
│ 日志/响应输出
|
||||
│
|
||||
┌──────────────┐ ┌────────┴────────┐ ┌──────────────┐
|
||||
│ UART1 │◄──►│ 指令解析器 │◄──►│ UART2 │
|
||||
│ (RF433) │ │ (CmdParser) │ │ (调试/上位机) │
|
||||
└──────────────┘ └────────┬────────┘ └──────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ IO状态监控 │ │ 继电器控制 │ │ RF433通信 │
|
||||
│ (DI1-4) │ │ (RL_Control)│ │ (透传模式) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### 1.4 任务调度模型
|
||||
|
||||
基于现有裸机架构,采用**时间片轮询**模型:
|
||||
|
||||
```c
|
||||
void main(void)
|
||||
{
|
||||
system_init();
|
||||
|
||||
while(1)
|
||||
{
|
||||
UART2_Print_Task(); // 发送缓冲区处理
|
||||
IO_Monitor_Task(); // IO扫描与去抖
|
||||
CmdParser_Task(); // 指令解析处理
|
||||
|
||||
__WFI(); // 等待中断,降低功耗
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**调度周期设计**:
|
||||
|
||||
| 任务 | 执行周期 | 说明 |
|
||||
| ----------------- | ---- | ----------------- |
|
||||
| `UART2_Print_Task` | 1ms | 发送缓冲区数据处理 |
|
||||
| `IO_Monitor_Task` | 10ms | IO扫描与去抖,足够采样间隔 |
|
||||
| `CmdParser_Task` | 每轮 | 指令解析,响应无需微秒级 |
|
||||
|
||||
***
|
||||
|
||||
## 二、模块详细设计
|
||||
|
||||
### 2.1 UART2_Print 模块
|
||||
|
||||
#### 2.1.1 模块职责
|
||||
|
||||
- 提供调试信息输出接口
|
||||
- 支持 `printf` 风格的格式化输出
|
||||
- 支持中断安全调用(通过环形缓冲区)
|
||||
|
||||
#### 2.1.2 头文件设计 (`uart2_print.h`)
|
||||
|
||||
```c
|
||||
#ifndef __UART2_PRINT_H
|
||||
#define __UART2_PRINT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define UART2_TX_BUFFER_SIZE 256
|
||||
|
||||
void UART2_Print_Init(void);
|
||||
void UART2_Print_Send(const uint8_t *data, uint16_t len);
|
||||
void UART2_Print_String(const char *str);
|
||||
void UART2_Print_Printf(const char *fmt, ...);
|
||||
void UART2_Print_Task(void);
|
||||
bool UART2_Print_IsBusy(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
#### 2.1.3 环形缓冲区实现
|
||||
|
||||
```c
|
||||
#include "uart2_print.h"
|
||||
#include "usart.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t buffer[UART2_TX_BUFFER_SIZE];
|
||||
volatile uint16_t head;
|
||||
volatile uint16_t tail;
|
||||
volatile bool is_sending;
|
||||
} ring_buffer_t;
|
||||
|
||||
static ring_buffer_t tx_ring = {0};
|
||||
|
||||
void UART2_Print_Init(void)
|
||||
{
|
||||
tx_ring.head = 0;
|
||||
tx_ring.tail = 0;
|
||||
tx_ring.is_sending = false;
|
||||
}
|
||||
|
||||
void UART2_Print_Send(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (len == 0) return;
|
||||
|
||||
__disable_irq();
|
||||
for (uint16_t i = 0; i < len; i++) {
|
||||
uint16_t next = (tx_ring.head + 1) % UART2_TX_BUFFER_SIZE;
|
||||
if (next == tx_ring.tail) {
|
||||
break;
|
||||
}
|
||||
tx_ring.buffer[tx_ring.head] = data[i];
|
||||
tx_ring.head = next;
|
||||
}
|
||||
__enable_irq();
|
||||
|
||||
if (!tx_ring.is_sending) {
|
||||
UART2_Print_Task();
|
||||
}
|
||||
}
|
||||
|
||||
void UART2_Print_String(const char *str)
|
||||
{
|
||||
UART2_Print_Send((const uint8_t *)str, strlen(str));
|
||||
}
|
||||
|
||||
void UART2_Print_Task(void)
|
||||
{
|
||||
if (tx_ring.is_sending || tx_ring.tail == tx_ring.head) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t byte = tx_ring.buffer[tx_ring.tail];
|
||||
tx_ring.tail = (tx_ring.tail + 1) % UART2_TX_BUFFER_SIZE;
|
||||
tx_ring.is_sending = true;
|
||||
|
||||
HAL_UART_Transmit_IT(&huart2, &byte, 1);
|
||||
}
|
||||
|
||||
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
|
||||
{
|
||||
if (huart->Instance == USART2) {
|
||||
tx_ring.is_sending = false;
|
||||
if (tx_ring.tail != tx_ring.head) {
|
||||
UART2_Print_Task();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.1.4 printf 重定向
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
|
||||
#if defined(__CC_ARM) || defined(__ARMCC_VERSION)
|
||||
int fputc(int ch, FILE *f)
|
||||
{
|
||||
UART2_Print_Send((uint8_t *)&ch, 1);
|
||||
return ch;
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### 2.2 ASCII文本指令协议
|
||||
|
||||
#### 2.2.1 指令格式设计
|
||||
|
||||
**设计原则**:
|
||||
- 人类可读,方便调试
|
||||
- 格式简单,易于解析
|
||||
- 兼容常见串口调试工具
|
||||
- **调试友好**:校验和FF为特权后门,跳过校验
|
||||
|
||||
**指令格式**:
|
||||
|
||||
```
|
||||
$CMD,param1,param2*CS\r\n
|
||||
|
||||
$ - 帧起始符
|
||||
CMD - 指令名称(2-4字符)
|
||||
, - 参数分隔符
|
||||
param - 参数(可选)
|
||||
* - 校验和起始符
|
||||
CS - 校验和(2位十六进制,从$到*之间所有字符的异或值)
|
||||
特权后门: CS=FF 时强制跳过校验,用于人工调试
|
||||
\r\n - 帧结束符
|
||||
```
|
||||
|
||||
#### 2.2.2 指令定义
|
||||
|
||||
| 指令 | 参数1 | 参数2 | 功能说明 | 示例 |
|
||||
| ----- | ------------ | --------- | -------------- | ----------------------- |
|
||||
| `RL` | 继电器号(1) | 0/1 | 继电器控制 | `$RL,1,1*FF\r\n` |
|
||||
| `DI` | 通道号(0-4) | (无) | 查询IO状态 | `$DI,1*FF\r\n` |
|
||||
| `FWD` | 目标端口(1) | 数据 | 数据转发 | `$FWD,1,Hello*FF\r\n` |
|
||||
| `ECHO`| (无) | (无) | 心跳测试 | `$ECHO*FF\r\n` |
|
||||
|
||||
**参数说明**:
|
||||
- `RL` 指令:参数2为 `0`=关闭,`1`=打开
|
||||
- `DI` 指令:参数1为 `0`=查询所有,`1-4`=查询指定通道
|
||||
|
||||
#### 2.2.3 完整指令示例
|
||||
|
||||
**示例1:控制继电器1打开**
|
||||
|
||||
```
|
||||
指令: $RL,1,1*FF\r\n
|
||||
|
||||
解析:
|
||||
- RL: 继电器控制指令
|
||||
- 参数1=1: 继电器编号1
|
||||
- 参数2=1: 打开
|
||||
- CS=FF: 跳过校验(调试模式)
|
||||
```
|
||||
|
||||
**示例2:控制继电器1关闭**
|
||||
|
||||
```
|
||||
指令: $RL,1,0*FF\r\n
|
||||
|
||||
解析:
|
||||
- 参数2=0: 关闭
|
||||
```
|
||||
|
||||
**示例3:查询DI1状态**
|
||||
|
||||
```
|
||||
指令: $DI,1*FF\r\n
|
||||
|
||||
响应: $OK,DI,1,1*XX\r\n (DI1=HIGH)
|
||||
或: $OK,DI,1,0*XX\r\n (DI1=LOW)
|
||||
```
|
||||
|
||||
**示例4:查询所有DI状态**
|
||||
|
||||
```
|
||||
指令: $DI,0*FF\r\n
|
||||
|
||||
响应: $OK,DI,1010*XX\r\n (DI1=1, DI2=0, DI3=1, DI4=0)
|
||||
```
|
||||
|
||||
**示例5:转发数据到RF433**
|
||||
|
||||
```
|
||||
指令: $FWD,1,Hello RF433*FF\r\n
|
||||
|
||||
说明: 将"Hello RF433"转发到UART1(RF433模块)
|
||||
```
|
||||
|
||||
**示例6:带校验和的正式指令**
|
||||
|
||||
```
|
||||
指令: $RL,1,1*4C\r\n
|
||||
|
||||
校验和计算: '$' ^ 'R' ^ 'L' ^ ',' ^ '1' ^ ',' ^ '1'
|
||||
= 0x24 ^ 0x52 ^ 0x4C ^ 0x2C ^ 0x31 ^ 0x2C ^ 0x31
|
||||
= 0x4C
|
||||
```
|
||||
|
||||
#### 2.2.4 响应格式
|
||||
|
||||
**成功响应**:
|
||||
|
||||
```
|
||||
$OK,RL,1,1*XX\r\n (继电器1已打开)
|
||||
$OK,DI,1,1*XX\r\n (DI1当前为HIGH)
|
||||
$OK,DI,1010*XX\r\n (所有DI状态)
|
||||
```
|
||||
|
||||
**失败响应**:
|
||||
|
||||
```
|
||||
$ERR,CMD*XX\r\n (未知指令)
|
||||
$ERR,CS*XX\r\n (校验和错误)
|
||||
$ERR,PARAM*XX\r\n (参数错误)
|
||||
```
|
||||
|
||||
**IO状态上报(主动上报)**:
|
||||
|
||||
```
|
||||
$DI_EVENT,1,1*XX\r\n (DI1变为HIGH)
|
||||
$DI_EVENT,3,0*XX\r\n (DI3变为LOW)
|
||||
```
|
||||
|
||||
#### 2.2.5 安全防护设计
|
||||
|
||||
**防护目标**:防止畸形数据导致单片机死机(HardFault)
|
||||
|
||||
**防护措施**:
|
||||
|
||||
| 风险场景 | 防护措施 | 实现方式 |
|
||||
| -------- | -------- | -------- |
|
||||
| 缓冲区溢出 | 边界检查 | 写入前检查索引是否越界 |
|
||||
| 超长数据帧 | 超时重置 | 长时间未收到完整帧则重置状态机 |
|
||||
| 非法字符 | 字符过滤 | 只接受合法ASCII字符 |
|
||||
| 内存越界 | 固定长度 | 使用固定大小缓冲区,拒绝超长数据 |
|
||||
|
||||
#### 2.2.6 指令解析器实现(带安全防护)
|
||||
|
||||
```c
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#define CMD_BUFFER_SIZE 64
|
||||
#define CMD_MAX_LEN 8
|
||||
#define PARAM_MAX_LEN 32
|
||||
#define PARSE_TIMEOUT_MS 1000
|
||||
|
||||
typedef enum {
|
||||
PARSE_IDLE,
|
||||
PARSE_CMD,
|
||||
PARSE_PARAM1,
|
||||
PARSE_PARAM2,
|
||||
PARSE_CHECKSUM,
|
||||
PARSE_COMPLETE
|
||||
} parse_state_t;
|
||||
|
||||
typedef struct {
|
||||
char cmd[CMD_MAX_LEN];
|
||||
char param1[PARAM_MAX_LEN];
|
||||
char param2[PARAM_MAX_LEN];
|
||||
uint8_t received_cs;
|
||||
uint8_t calculated_cs;
|
||||
bool valid;
|
||||
bool skip_checksum;
|
||||
} cmd_frame_t;
|
||||
|
||||
static parse_state_t parse_state = PARSE_IDLE;
|
||||
static cmd_frame_t current_frame;
|
||||
static uint8_t field_index = 0;
|
||||
static uint8_t checksum_acc = 0;
|
||||
static uint32_t last_rx_tick = 0;
|
||||
|
||||
static void reset_parser(void)
|
||||
{
|
||||
parse_state = PARSE_IDLE;
|
||||
field_index = 0;
|
||||
checksum_acc = 0;
|
||||
memset(¤t_frame, 0, sizeof(current_frame));
|
||||
}
|
||||
|
||||
static bool is_valid_cmd_char(char c)
|
||||
{
|
||||
return isupper(c) || isdigit(c);
|
||||
}
|
||||
|
||||
static bool is_valid_param_char(char c)
|
||||
{
|
||||
return isprint(c) && c != '*' && c != '\r' && c != '\n';
|
||||
}
|
||||
|
||||
static uint8_t hex_to_byte(char high, char low)
|
||||
{
|
||||
uint8_t val = 0;
|
||||
|
||||
if (high >= '0' && high <= '9') val = (high - '0') << 4;
|
||||
else if (high >= 'A' && high <= 'F') val = (high - 'A' + 10) << 4;
|
||||
else if (high >= 'a' && high <= 'f') val = (high - 'a' + 10) << 4;
|
||||
|
||||
if (low >= '0' && low <= '9') val |= (low - '0');
|
||||
else if (low >= 'A' && low <= 'F') val |= (low - 'A' + 10);
|
||||
else if (low >= 'a' && low <= 'f') val |= (low - 'a' + 10);
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
void CmdParser_FeedByte(uint8_t byte, uint32_t current_tick)
|
||||
{
|
||||
if (parse_state != PARSE_IDLE) {
|
||||
if (current_tick - last_rx_tick > PARSE_TIMEOUT_MS) {
|
||||
reset_parser();
|
||||
return;
|
||||
}
|
||||
}
|
||||
last_rx_tick = current_tick;
|
||||
|
||||
switch (parse_state) {
|
||||
case PARSE_IDLE:
|
||||
if (byte == '$') {
|
||||
reset_parser();
|
||||
parse_state = PARSE_CMD;
|
||||
}
|
||||
break;
|
||||
|
||||
case PARSE_CMD:
|
||||
if (byte == ',') {
|
||||
current_frame.cmd[field_index] = '\0';
|
||||
parse_state = PARSE_PARAM1;
|
||||
field_index = 0;
|
||||
} else if (byte == '*') {
|
||||
current_frame.cmd[field_index] = '\0';
|
||||
parse_state = PARSE_CHECKSUM;
|
||||
field_index = 0;
|
||||
} else if (is_valid_cmd_char(byte)) {
|
||||
if (field_index < CMD_MAX_LEN - 1) {
|
||||
current_frame.cmd[field_index++] = byte;
|
||||
checksum_acc ^= byte;
|
||||
} else {
|
||||
reset_parser();
|
||||
}
|
||||
} else {
|
||||
reset_parser();
|
||||
}
|
||||
break;
|
||||
|
||||
case PARSE_PARAM1:
|
||||
if (byte == ',') {
|
||||
current_frame.param1[field_index] = '\0';
|
||||
parse_state = PARSE_PARAM2;
|
||||
field_index = 0;
|
||||
} else if (byte == '*') {
|
||||
current_frame.param1[field_index] = '\0';
|
||||
parse_state = PARSE_CHECKSUM;
|
||||
field_index = 0;
|
||||
} else if (is_valid_param_char(byte)) {
|
||||
if (field_index < PARAM_MAX_LEN - 1) {
|
||||
current_frame.param1[field_index++] = byte;
|
||||
checksum_acc ^= byte;
|
||||
} else {
|
||||
reset_parser();
|
||||
}
|
||||
} else {
|
||||
reset_parser();
|
||||
}
|
||||
break;
|
||||
|
||||
case PARSE_PARAM2:
|
||||
if (byte == '*') {
|
||||
current_frame.param2[field_index] = '\0';
|
||||
parse_state = PARSE_CHECKSUM;
|
||||
field_index = 0;
|
||||
} else if (is_valid_param_char(byte)) {
|
||||
if (field_index < PARAM_MAX_LEN - 1) {
|
||||
current_frame.param2[field_index++] = byte;
|
||||
checksum_acc ^= byte;
|
||||
} else {
|
||||
reset_parser();
|
||||
}
|
||||
} else {
|
||||
reset_parser();
|
||||
}
|
||||
break;
|
||||
|
||||
case PARSE_CHECKSUM:
|
||||
if (byte == '\n') {
|
||||
current_frame.received_cs = hex_to_byte(
|
||||
current_frame.cmd[0],
|
||||
current_frame.cmd[1]
|
||||
);
|
||||
current_frame.calculated_cs = checksum_acc;
|
||||
current_frame.skip_checksum = (current_frame.received_cs == 0xFF);
|
||||
|
||||
if (current_frame.skip_checksum ||
|
||||
current_frame.received_cs == current_frame.calculated_cs) {
|
||||
current_frame.valid = true;
|
||||
parse_state = PARSE_COMPLETE;
|
||||
} else {
|
||||
reset_parser();
|
||||
}
|
||||
} else if (byte != '\r') {
|
||||
if (field_index < 2) {
|
||||
current_frame.cmd[field_index++] = byte;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PARSE_COMPLETE:
|
||||
reset_parser();
|
||||
if (byte == '$') {
|
||||
parse_state = PARSE_CMD;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool CmdParser_HasCompleteFrame(cmd_frame_t *frame)
|
||||
{
|
||||
if (parse_state == PARSE_COMPLETE && current_frame.valid) {
|
||||
if (frame) {
|
||||
memcpy(frame, ¤t_frame, sizeof(cmd_frame_t));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CmdParser_Acknowledge(void)
|
||||
{
|
||||
reset_parser();
|
||||
}
|
||||
```
|
||||
|
||||
**安全防护要点说明**:
|
||||
|
||||
1. **缓冲区边界检查**:每次写入前检查 `field_index < MAX_LEN`
|
||||
2. **超时重置**:超过1秒未收到完整帧,自动重置状态机
|
||||
3. **字符过滤**:只接受合法字符,非法字符触发重置
|
||||
4. **固定长度缓冲区**:不使用动态内存分配,避免内存碎片
|
||||
5. **校验和特权**:`CS=FF` 时跳过校验,方便人工调试
|
||||
|
||||
***
|
||||
|
||||
### 2.3 IO状态监控模块
|
||||
|
||||
#### 2.3.1 检测方式
|
||||
|
||||
**推荐方案:定时扫描 + 软件去抖**
|
||||
|
||||
| 方案 | 优点 | 缺点 | 适用场景 |
|
||||
| ---- | --------- | --------------- | ------ |
|
||||
| 外部中断 | 响应快 | 需要额外中断资源,硬件去抖复杂 | 低频事件 |
|
||||
| 定时扫描 | 简单可靠,统一管理 | 有扫描延迟 | 多路IO监控 |
|
||||
|
||||
#### 2.3.2 软件去抖实现
|
||||
|
||||
```c
|
||||
#define DEBOUNCE_COUNT 3
|
||||
#define IO_CHANNEL_COUNT 4
|
||||
|
||||
typedef struct {
|
||||
GPIO_TypeDef *port;
|
||||
uint16_t pin;
|
||||
uint8_t current_state;
|
||||
uint8_t debounce_counter;
|
||||
uint8_t last_raw_state;
|
||||
} io_channel_t;
|
||||
|
||||
static io_channel_t di_channels[IO_CHANNEL_COUNT] = {
|
||||
{GPIOB, GPIO_PIN_4, 0, 0, 0},
|
||||
{GPIOB, GPIO_PIN_5, 0, 0, 0},
|
||||
{GPIOB, GPIO_PIN_6, 0, 0, 0},
|
||||
{GPIOB, GPIO_PIN_7, 0, 0, 0}
|
||||
};
|
||||
|
||||
static uint32_t last_scan_tick = 0;
|
||||
|
||||
void IO_Monitor_Task(void)
|
||||
{
|
||||
if (HAL_GetTick() - last_scan_tick < 10) {
|
||||
return;
|
||||
}
|
||||
last_scan_tick = HAL_GetTick();
|
||||
|
||||
for (int i = 0; i < IO_CHANNEL_COUNT; i++) {
|
||||
io_channel_t *ch = &di_channels[i];
|
||||
|
||||
uint8_t raw_state = HAL_GPIO_ReadPin(ch->port, ch->pin) ? 1 : 0;
|
||||
|
||||
if (raw_state != ch->last_raw_state) {
|
||||
ch->debounce_counter = 0;
|
||||
ch->last_raw_state = raw_state;
|
||||
} else {
|
||||
if (ch->debounce_counter < DEBOUNCE_COUNT) {
|
||||
ch->debounce_counter++;
|
||||
} else if (ch->current_state != raw_state) {
|
||||
ch->current_state = raw_state;
|
||||
IO_StateChangeHandler(i, raw_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IO_StateChangeHandler(uint8_t channel, uint8_t state)
|
||||
{
|
||||
UART2_Print_Printf("$DI_EVENT,%d,%d*XX\r\n",
|
||||
channel + 1,
|
||||
state);
|
||||
}
|
||||
|
||||
uint8_t IO_Monitor_GetAllStates(void)
|
||||
{
|
||||
uint8_t states = 0;
|
||||
for (int i = 0; i < IO_CHANNEL_COUNT; i++) {
|
||||
if (di_channels[i].current_state) {
|
||||
states |= (1 << i);
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3.3 IO监控模块接口
|
||||
|
||||
```c
|
||||
#ifndef __IO_MONITOR_H
|
||||
#define __IO_MONITOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void IO_Monitor_Init(void);
|
||||
void IO_Monitor_Task(void);
|
||||
uint8_t IO_Monitor_GetState(uint8_t channel);
|
||||
uint8_t IO_Monitor_GetAllStates(void);
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### 2.4 继电器控制模块
|
||||
|
||||
#### 2.4.1 模块接口
|
||||
|
||||
```c
|
||||
#ifndef __RELAY_CONTROL_H
|
||||
#define __RELAY_CONTROL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
void Relay_Init(void);
|
||||
void Relay_SetState(uint8_t relay_id, bool state);
|
||||
bool Relay_GetState(uint8_t relay_id);
|
||||
void Relay_Toggle(uint8_t relay_id);
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
#### 2.4.2 实现代码
|
||||
|
||||
```c
|
||||
#include "relay_control.h"
|
||||
#include "main.h"
|
||||
|
||||
void Relay_Init(void)
|
||||
{
|
||||
HAL_GPIO_WritePin(RL_Control_GPIO_Port, RL_Control_Pin, GPIO_PIN_RESET);
|
||||
}
|
||||
|
||||
void Relay_SetState(uint8_t relay_id, bool state)
|
||||
{
|
||||
if (relay_id == 1) {
|
||||
HAL_GPIO_WritePin(RL_Control_GPIO_Port, RL_Control_Pin,
|
||||
state ? GPIO_PIN_SET : GPIO_PIN_RESET);
|
||||
}
|
||||
}
|
||||
|
||||
bool Relay_GetState(uint8_t relay_id)
|
||||
{
|
||||
if (relay_id == 1) {
|
||||
return HAL_GPIO_ReadPin(RL_Control_GPIO_Port, RL_Control_Pin) ? true : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Relay_Toggle(uint8_t relay_id)
|
||||
{
|
||||
if (relay_id == 1) {
|
||||
HAL_GPIO_TogglePin(RL_Control_GPIO_Port, RL_Control_Pin);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 三、关键数据结构与接口
|
||||
|
||||
### 3.1 简化的数据结构
|
||||
|
||||
```c
|
||||
#ifndef __APP_TYPES_H
|
||||
#define __APP_TYPES_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define IO_CHANNEL_COUNT 4
|
||||
|
||||
typedef enum {
|
||||
UART_PORT_1 = 1,
|
||||
UART_PORT_2 = 2,
|
||||
UART_PORT_3 = 3
|
||||
} uart_port_t;
|
||||
|
||||
typedef struct {
|
||||
char cmd[8];
|
||||
char param1[16];
|
||||
char param2[32];
|
||||
uart_port_t source;
|
||||
bool valid;
|
||||
} cmd_frame_t;
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### 3.2 设计说明
|
||||
|
||||
| 数据结构 | 用途 | 设计理由 |
|
||||
| -------- | ---- | -------- |
|
||||
| `uart_port_t` | 标识串口 | 简单枚举,UART1=RF433,UART2=调试口,UART3=预留 |
|
||||
| `cmd_frame_t` | 存储解析后的指令 | 固定长度字符数组,避免动态内存分配 |
|
||||
|
||||
**为什么不需要复杂的数据结构?**
|
||||
|
||||
1. **ASCII指令格式**:人类可读,直接用字符串处理即可
|
||||
2. **裸机轮询架构**:不需要消息队列,直接在主循环处理
|
||||
3. **单一职责**:每个模块功能单一,不需要复杂的抽象
|
||||
|
||||
***
|
||||
|
||||
## 四、实施路线图
|
||||
|
||||
### 4.1 分阶段开发计划
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 开发阶段时间线 │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 第一阶段 (基础框架) 第二阶段 (核心功能) 第三阶段 (扩展) │
|
||||
│ ├─ UART2_Print ├─ ASCII指令解析 ├─ RF433透传 │
|
||||
│ ├─ IO监控模块 ├─ 继电器控制 ├─ UART3扩展 │
|
||||
│ └─ 基础日志输出 └─ 指令响应 └─ 配置存储 │
|
||||
│ │
|
||||
│ 验证点: 验证点: 验证点: │
|
||||
│ · 串口输出正常 · 指令响应正确 · 无线通信正常 │
|
||||
│ · IO状态上报 · 继电器动作 · 长时间稳定性 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 第一阶段:基础框架搭建
|
||||
|
||||
**目标**:实现调试输出能力和IO状态监控
|
||||
|
||||
**任务清单**:
|
||||
|
||||
| 序号 | 任务 | 预计工作量 | 依赖 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1.1 | 实现 `UART2_Print` 模块 | 1.5h | 无 |
|
||||
| 1.2 | 实现 `printf` 重定向 | 0.5h | 1.1 |
|
||||
| 1.3 | 实现 `IO_Monitor` 模块 | 1.5h | 无 |
|
||||
| 1.4 | 集成到 `main.c` 主循环 | 0.5h | 1.1, 1.3 |
|
||||
| 1.5 | 编写测试代码验证 | 0.5h | 1.4 |
|
||||
|
||||
**验证方法**:
|
||||
|
||||
```c
|
||||
void phase1_test(void)
|
||||
{
|
||||
UART2_Print_Init();
|
||||
IO_Monitor_Init();
|
||||
|
||||
printf("Phase 1 Test Start\r\n");
|
||||
printf("System Clock: %d MHz\r\n", SystemCoreClock / 1000000);
|
||||
|
||||
while (1) {
|
||||
IO_Monitor_Task();
|
||||
UART2_Print_Task();
|
||||
|
||||
static uint32_t last_tick = 0;
|
||||
if (HAL_GetTick() - last_tick > 5000) {
|
||||
last_tick = HAL_GetTick();
|
||||
uint8_t states = IO_Monitor_GetAllStates();
|
||||
printf("DI States: 0x%02X\r\n", states);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] UART2能正常输出调试信息
|
||||
- [ ] `printf` 函数可用
|
||||
- [ ] DI1-DI4状态变化能实时上报
|
||||
- [ ] 软件去抖功能正常
|
||||
|
||||
***
|
||||
|
||||
### 4.3 第二阶段:指令解析与继电器控制
|
||||
|
||||
**目标**:实现ASCII指令解析和继电器控制
|
||||
|
||||
**任务清单**:
|
||||
|
||||
| 序号 | 任务 | 预计工作量 | 依赖 |
|
||||
| --- | --- | --- | --- |
|
||||
| 2.1 | 实现ASCII指令解析器 | 2h | 第一阶段 |
|
||||
| 2.2 | 实现继电器控制模块 | 0.5h | 无 |
|
||||
| 2.3 | 实现指令响应机制 | 1h | 2.1, 2.2 |
|
||||
| 2.4 | UART2接收中断集成 | 0.5h | 2.1 |
|
||||
| 2.5 | 端到端测试 | 0.5h | 2.4 |
|
||||
|
||||
**验证方法**:
|
||||
|
||||
```c
|
||||
void phase2_test(void)
|
||||
{
|
||||
UART2_Print_Init();
|
||||
IO_Monitor_Init();
|
||||
CmdParser_Init();
|
||||
Relay_Init();
|
||||
|
||||
printf("Phase 2 Test Start\r\n");
|
||||
|
||||
while (1) {
|
||||
IO_Monitor_Task();
|
||||
UART2_Print_Task();
|
||||
CmdParser_Task();
|
||||
}
|
||||
|
||||
// 手动测试:通过串口发送指令
|
||||
// 打开继电器1: $RL,1,1*FF\r\n
|
||||
// 关闭继电器1: $RL,1,0*FF\r\n
|
||||
// 查询DI1状态: $DI,1*FF\r\n
|
||||
// 查询所有DI: $DI,0*FF\r\n
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] 能正确解析ASCII指令
|
||||
- [ ] 校验和验证正常
|
||||
- [ ] 继电器能按指令开关
|
||||
- [ ] 指令执行后有响应输出
|
||||
- [ ] 错误指令能被识别并返回错误信息
|
||||
|
||||
***
|
||||
|
||||
### 4.4 第三阶段:RF433透传与扩展
|
||||
|
||||
**目标**:实现RF433无线透传功能
|
||||
|
||||
**任务清单**:
|
||||
|
||||
| 序号 | 任务 | 预计工作量 | 依赖 |
|
||||
| --- | --- | --- | --- |
|
||||
| 3.1 | 实现FWD转发指令处理 | 1h | 第二阶段 |
|
||||
| 3.2 | 集成RF433模块驱动 | 1h | 3.1 |
|
||||
| 3.3 | 实现双向透传 | 1h | 3.2 |
|
||||
| 3.4 | 压力测试与优化 | 1h | 3.3 |
|
||||
|
||||
**验证方法**:
|
||||
|
||||
```c
|
||||
// 测试场景:
|
||||
// 1. UART2发送: $FWD,1,Hello RF433*FF\r\n
|
||||
// 验证UART1(RF433)是否收到 "Hello RF433"
|
||||
//
|
||||
// 2. RF433接收数据后,自动转发到UART2
|
||||
// 验证UART2是否收到RF433的数据
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
|
||||
- [ ] UART2→RF433转发正常
|
||||
- [ ] RF433→UART2转发正常
|
||||
- [ ] 连续转发100次无丢包
|
||||
- [ ] 长时间运行稳定(>30分钟)
|
||||
|
||||
***
|
||||
|
||||
### 4.5 文件组织
|
||||
|
||||
```
|
||||
Core/
|
||||
├── Inc/
|
||||
│ ├── uart2_print.h # 调试打印模块
|
||||
│ ├── io_monitor.h # IO监控模块
|
||||
│ ├── cmd_parser.h # 指令解析模块
|
||||
│ ├── relay_control.h # 继电器控制模块
|
||||
│ └── app_types.h # 公共类型定义
|
||||
├── Src/
|
||||
│ ├── main.c # 主程序(修改)
|
||||
│ ├── uart2_print.c # 调试打印实现
|
||||
│ ├── io_monitor.c # IO监控实现
|
||||
│ ├── cmd_parser.c # 指令解析实现
|
||||
│ └── relay_control.c # 继电器控制实现
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### 4.6 风险与缓解措施
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
| --- | --- | --- |
|
||||
| 环形缓冲区溢出 | 数据丢失 | 增加缓冲区大小,添加溢出检测 |
|
||||
| 指令解析错误 | 功能异常 | 完善状态机,添加超时重置机制 |
|
||||
| 继电器控制时序 | 硬件损坏 | 添加最小间隔限制 |
|
||||
|
||||
***
|
||||
|
||||
## 五、总结
|
||||
|
||||
### 架构设计要点
|
||||
|
||||
1. **简化分层**:应用层 → 服务层 → 驱动层,职责清晰
|
||||
2. **ASCII指令**:人类可读,方便调试,易于扩展
|
||||
3. **时间片轮询**:基于现有裸机架构,无需RTOS
|
||||
|
||||
### 协议设计亮点
|
||||
|
||||
1. **ASCII文本格式**:`$CMD,param1,param2*CS\r\n`,简单直观
|
||||
2. **异或校验**:计算简单,适合嵌入式系统
|
||||
3. **统一响应格式**:`$OK,...` 或 `$ERR,...`
|
||||
|
||||
### 关键技术决策
|
||||
|
||||
| 决策点 | 选择 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| 指令格式 | ASCII文本 | 人类可读,方便调试,兼容串口工具 |
|
||||
| IO检测方式 | 定时扫描 | 4路统一管理,软件去抖灵活 |
|
||||
| 打印缓冲 | 环形缓冲区 | ISR安全,非阻塞发送 |
|
||||
|
||||
### 下一步行动
|
||||
|
||||
建议按照以下顺序开始编码:
|
||||
|
||||
1. 实现 `UART2_Print` 模块,验证调试输出
|
||||
2. 实现 `IO_Monitor` 模块,验证IO监控
|
||||
3. 实现 `CmdParser` 模块,验证指令解析
|
||||
4. 实现 `Relay_Control` 模块,验证继电器控制
|
||||
Reference in New Issue
Block a user