Просмотр исходного кода

实现 2026 PLSR 指令及最小 Modbus RTU

codex/plsr-2026-minimal
ywh 1 месяц назад
Родитель
Сommit
c7b851659e
17 измененных файлов: 9438 добавлений и 965 удалений
  1. +4
    -1
      Core/Inc/stm32f4xx_it.h
  2. +11
    -56
      Core/Src/main.c
  3. +48
    -0
      EWARM/Modbus.ewp
  4. +2
    -2
      EWARM/stm32f407xx_flash.icf
  5. +31
    -1
      HostComputer/README.md
  6. +935
    -0
      HostComputer/plsr_control_panel.py
  7. +1162
    -0
      HostComputer/plsr_modbus_product_test.py
  8. +3
    -0
      Middlewares/Third_Party/Micrium/Config/app_cfg.h
  9. +2
    -128
      Modbus/Inc/modbus_rtu_slave.h
  10. +256
    -777
      Modbus/Src/modbus_rtu_slave.c
  11. +94
    -0
      PLSR/Inc/plsr.h
  12. +3599
    -0
      PLSR/Src/plsr.c
  13. +49
    -0
      PLSR/Src/plsr_internal.h
  14. +34
    -0
      PLSR/Src/plsr_platform.h
  15. +1202
    -0
      PLSR/Src/plsr_platform_f407.c
  16. +42
    -0
      tests/plsr_host/run_tests.ps1
  17. +1964
    -0
      tests/plsr_host/test_plsr_host.c

+ 4
- 1
Core/Inc/stm32f4xx_it.h Просмотреть файл

@@ -60,7 +60,10 @@ void DMA2_Stream2_IRQHandler(void);
void OTG_FS_IRQHandler(void); void OTG_FS_IRQHandler(void);
void DMA2_Stream7_IRQHandler(void); void DMA2_Stream7_IRQHandler(void);
/* USER CODE BEGIN EFP */ /* USER CODE BEGIN EFP */

void TIM1_UP_TIM10_IRQHandler(void);
void TIM8_UP_TIM13_IRQHandler(void);
void TIM1_TRG_COM_TIM11_IRQHandler(void);
void TIM8_TRG_COM_TIM14_IRQHandler(void);
/* USER CODE END EFP */ /* USER CODE END EFP */


#ifdef __cplusplus #ifdef __cplusplus


+ 11
- 56
Core/Src/main.c Просмотреть файл

@@ -24,7 +24,7 @@
/* USER CODE BEGIN Includes */ /* USER CODE BEGIN Includes */
#include "ucos_ii.h" #include "ucos_ii.h"
#include "modbus_rtu_slave.h" #include "modbus_rtu_slave.h"
#include "stdio.h"
#include "plsr.h"
/* USER CODE END Includes */ /* USER CODE END Includes */


/* Private typedef -----------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/
@@ -46,7 +46,6 @@
UART_HandleTypeDef huart1; UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx; DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx; DMA_HandleTypeDef hdma_usart1_tx;
static volatile uint8_t ConnectFlag;
/* USER CODE BEGIN PV */ /* USER CODE BEGIN PV */


static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE]; static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];
@@ -54,7 +53,6 @@ static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];


/* Private function prototypes -----------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void); void SystemClock_Config(void);
static HAL_StatusTypeDef BackupSramInit(void);
static void MX_GPIO_Init(void); static void MX_GPIO_Init(void);
static void MX_DMA_Init(void); static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void); static void MX_USART1_UART_Init(void);
@@ -68,55 +66,17 @@ static void AppTaskStart(void *pArg);


static void AppTaskStart(void *pArg) static void AppTaskStart(void *pArg)
{ {

(void)pArg; (void)pArg;


/*
* F4作为Modbus RTU从站,触摸屏作为主站
* 初始化函数会立即启动USART1的DMA空闲接收
*/
(void)ModbusSlaveInit(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS);
//ModbusRetainedRegistersLoad();
if (ModbusSlaveInit(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS) != HAL_OK)
{
Error_Handler();
}

while (1) while (1)
{ {
/*
* 维护几个供触摸屏首次联调读取的保持寄存器:
*/
// (void)ModbusSlaveSetHoldingRegister( // OS时钟
// HMI_REG_UPTIME_SECONDS, (uint16_t)(OSTimeGet() / OS_TICKS_PER_SEC));
// (void)ModbusSlaveSetHoldingRegister( // 有效帧计数
// HMI_REG_RX_FRAME_COUNT,
// (uint16_t)ModbusSlaveStatistics.validFrameCount);

// (void)ModbusSlaveSetHoldingRegister( // 从站地址不匹配计数
// D3, (uint16_t)ModbusSlaveStatistics.ignoredAddressCount);
// (void)ModbusSlaveSetHoldingRegister( // 非法功能码计数
// D4, (uint16_t)ModbusSlaveStatistics.illegalFunctionCount);

// (void)ModbusSlaveSetHoldingRegister( // 非法地址计数
// D5, (uint16_t)ModbusSlaveStatistics.illegalAddressCount);

// (void)ModbusSlaveSetHoldingRegister( // 非法数据值计数
// D6, (uint16_t)ModbusSlaveStatistics.illegalValueCount);

/*
* 每1ms轮询一次,
*/
PlsrPoll1ms();
ModbusSlavePoll(); ModbusSlavePoll();
// ModbusRetainedRegistersPoll();

if (ModbusSlaveIsConnected(MODBUS_CONNECTION_TIMEOUT_MS) != 0U)
{
/* 主站在线 */
ConnectFlag = 1;
}
else
{
/* 主站超时或断开 */
ConnectFlag = 0;
}

OSTimeDly(1U); OSTimeDly(1U);
} }
} }
@@ -156,7 +116,10 @@ int main(void)
MX_DMA_Init(); MX_DMA_Init();
MX_USB_DEVICE_Init(); MX_USB_DEVICE_Init();
MX_USART1_UART_Init(); MX_USART1_UART_Init();
(void)BackupSramInit();
if (PlsrInit() == 0U)
{
Error_Handler();
}


/* USER CODE BEGIN 2 */ /* USER CODE BEGIN 2 */
INT8U osError; INT8U osError;
@@ -188,14 +151,6 @@ int main(void)
/* USER CODE END 3 */ /* USER CODE END 3 */
} }


static HAL_StatusTypeDef BackupSramInit(void)
{
HAL_PWR_EnableBkUpAccess();
__HAL_RCC_BKPSRAM_CLK_ENABLE();

return HAL_PWREx_EnableBkUpReg();
}

/** /**
* @brief System Clock Configuration * @brief System Clock Configuration
* @retval None * @retval None


+ 48
- 0
EWARM/Modbus.ewp Просмотреть файл

@@ -360,6 +360,7 @@
<state>$PROJ_DIR$/../Drivers/CMSIS/Device/ST/STM32F4xx/Include</state> <state>$PROJ_DIR$/../Drivers/CMSIS/Device/ST/STM32F4xx/Include</state>
<state>$PROJ_DIR$/../Drivers/CMSIS/Include</state> <state>$PROJ_DIR$/../Drivers/CMSIS/Include</state>
<state>$PROJ_DIR$\..\Modbus\Inc</state> <state>$PROJ_DIR$\..\Modbus\Inc</state>
<state>$PROJ_DIR$\..\PLSR\Inc</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\Config</state> <state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\Config</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Source</state> <state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Source</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Ports\ARM-Cortex-M4\IAR</state> <state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Ports\ARM-Cortex-M4\IAR</state>
@@ -1260,6 +1261,53 @@
</file> </file>
</group> </group>
</group> </group>
<group>
<name>PLSR</name>
<file>
<name>$PROJ_DIR$\..\PLSR\Inc\plsr.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\PLSR\Src\plsr_internal.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\PLSR\Src\plsr_platform.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\PLSR\Src\plsr.c</name>
<configuration>
<name>Modbus</name>
<settings>
<name>ICCARM</name>
<data>
<version>35</version>
<wantNonLocal>0</wantNonLocal>
<debug>1</debug>
<option>
<name>CCOptLevel</name>
<state>3</state>
</option>
<option>
<name>CCOptStrategy</name>
<version>0</version>
<state>2</state>
</option>
<option>
<name>CCOptLevelSlave</name>
<state>3</state>
</option>
<option>
<name>CCOptStrategySlave</name>
<version>0</version>
<state>2</state>
</option>
</data>
</settings>
</configuration>
</file>
<file>
<name>$PROJ_DIR$\..\PLSR\Src\plsr_platform_f407.c</name>
</file>
</group>
<group> <group>
<name>Modbus</name> <name>Modbus</name>
<file> <file>


+ 2
- 2
EWARM/stm32f407xx_flash.icf Просмотреть файл

@@ -5,7 +5,7 @@
define symbol __ICFEDIT_intvec_start__ = 0x08000000; define symbol __ICFEDIT_intvec_start__ = 0x08000000;
/*-Memory Regions-*/ /*-Memory Regions-*/
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000; define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;
define symbol __ICFEDIT_region_ROM_end__ = 0x080FFFFF;
define symbol __ICFEDIT_region_ROM_end__ = 0x080BFFFF;
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000; define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__ = 0x2001FFFF; define symbol __ICFEDIT_region_RAM_end__ = 0x2001FFFF;
define symbol __ICFEDIT_region_CCMRAM_start__ = 0x10000000; define symbol __ICFEDIT_region_CCMRAM_start__ = 0x10000000;
@@ -35,4 +35,4 @@ place in RAM_region { readwrite,
place in CCMRAM_region place in CCMRAM_region
{ {
section .ccmram section .ccmram
};
};

+ 31
- 1
HostComputer/README.md Просмотреть файл

@@ -1,4 +1,34 @@
# Modbus RTU T1.5/T3.5测试上位机
# 上位机工具

## PLSR 控制面板

`plsr_control_panel.py`用于配置和控制2026版PLSR指令。串口参数固定为
9600、8位数据、偶校验、1位停止位(8E1),从站地址可选1~247。

在仓库根目录运行:

```powershell
py -B HostComputer\plsr_control_panel.py
```

界面可读写公共参数和全部10段参数,轮询显示累计位置、当前频率、运行
状态、当前段和错误码,并可发送启动、停止、清零命令。Y0~Y3是同一条
PLSR逻辑的可选脉冲输出,不是四条可同时运行的独立PLSR指令;方向输出为
Y12~Y15,WAIT/EXT输入为X4或X5。

离线自测不会创建窗口或打开串口:

```powershell
py -B HostComputer\plsr_control_panel.py --self-test
```

依赖安装:

```powershell
py -m pip install -r HostComputer\requirements.txt
```

## Modbus RTU T1.5/T3.5测试上位机


## 直接运行EXE ## 直接运行EXE




+ 935
- 0
HostComputer/plsr_control_panel.py Просмотреть файл

@@ -0,0 +1,935 @@
"""Tkinter control panel for the 2026 PLSR Modbus register map."""

from __future__ import annotations

import argparse
import queue
import threading
import time
import tkinter as tk
import tkinter.font as tkfont
from tkinter import messagebox, ttk

import serial
from serial.tools import list_ports

# Keep one RTU implementation for the board tests and this operator panel.
from plsr_modbus_product_test import (
COMMAND_CLEAR,
COMMAND_START,
COMMAND_STOP,
COMMON_WORDS,
CONFIG_BASE,
CONTROL,
ModbusException,
RtuClient,
SEGMENT_1_BASE,
SEGMENT_WORDS,
TestFailure,
read_status,
split_i32,
split_u32,
)


BAUD_RATE = 9600
SERIAL_TIMEOUT_SECONDS = 0.8
STATUS_POLL_SECONDS = 0.25
SEGMENT_COUNT = 10
SEGMENT_STRIDE = 0x10
MAX_FREQUENCY_HZ = 100_000

PULSE_OPTIONS = (
("Y0 (0)", 0),
("Y1 (1)", 1),
("Y2 (2)", 2),
("Y3 (3)", 3),
)
DIRECTION_OPTIONS = (
("Y12 (0)", 0),
("Y13 (1)", 1),
("Y14 (2)", 2),
("Y15 (3)", 3),
)
INPUT_OPTIONS = (("X4 (0)", 0), ("X5 (1)", 1))
SEND_OPTIONS = (("脉冲发送完毕 (0)", 0), ("后续段 (1)", 1))
LOGIC_OPTIONS = (("正逻辑 (0)", 0), ("负逻辑 (1)", 1))
CURVE_OPTIONS = (("直线 (0)", 0), ("S 曲线 (1)", 1), ("正弦曲线 (2)", 2))
POSITION_OPTIONS = (("相对位置 (0)", 0), ("绝对位置 (1)", 1))
WAIT_OPTIONS = (
("WAIT 时间 (0)", 0),
("WAIT 信号 (1)", 1),
("ACT 时间 (2)", 2),
("EXT 信号 (3)", 3),
("EXT 或完成 (4)", 4),
)

STATUS_NAMES = {
0: "未初始化",
1: "空闲",
2: "加速",
3: "运行",
4: "减速",
5: "等待",
6: "暂停",
7: "完成",
8: "停止",
9: "错误",
}
ERROR_NAMES = {
0: "无错误",
1: "状态转换非法",
2: "资源冲突",
3: "资源非法",
4: "定时器错误",
5: "计数错误",
6: "正限位",
7: "负限位",
8: "急停",
9: "内部错误",
}


def option_labels(options):
return tuple(label for label, _value in options)


def option_label(options, value):
for label, candidate in options:
if candidate == value:
return label
return str(value)


def option_value(options, label, field_name):
for candidate_label, value in options:
if candidate_label == label:
return value
raise ValueError("%s 不是有效选项" % field_name)


def parse_integer(text, field_name, minimum, maximum):
raw = text.strip()
if not raw:
raise ValueError("%s 不能为空" % field_name)
try:
value = int(raw, 0)
except ValueError:
try:
value = int(raw, 10)
except ValueError as error:
raise ValueError("%s 必须是整数" % field_name) from error
if value < minimum or value > maximum:
raise ValueError(
"%s 必须在 %d 到 %d 之间" % (field_name, minimum, maximum)
)
return value


class SerialWorker(threading.Thread):
"""Own the serial port and report all results through a queue."""

def __init__(self):
super().__init__(name="plsr-serial", daemon=True)
self.commands = queue.Queue()
self.results = queue.Queue()
self.stop_event = threading.Event()
self.client = None
self.next_status_poll = 0.0

def submit(self, command, **payload):
self.commands.put((command, payload))

def close(self):
self.stop_event.set()
self.commands.put(("shutdown", {}))

def _emit(self, event, **payload):
self.results.put((event, payload))

def _disconnect(self, notify=True, reason=""):
client = self.client
self.client = None
if client is not None:
try:
client.__exit__(None, None, None)
except (OSError, serial.SerialException):
pass
if notify:
self._emit("connection", connected=False, reason=reason)

def _require_client(self):
if self.client is None:
raise RuntimeError("串口尚未连接")
return self.client

def _read_configuration(self):
client = self._require_client()
common = client.read_holding(CONFIG_BASE, COMMON_WORDS)
segments = []
for index in range(SEGMENT_COUNT):
address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
segments.append(client.read_holding(address, SEGMENT_WORDS))
self._emit("configuration", common=common, segments=segments)

def _handle_connection_error(self, operation, error):
self._disconnect(notify=True, reason=str(error))
self._emit("error", operation=operation, message=str(error), modal=True)

def _handle_command(self, command, payload):
if command == "connect":
self._disconnect(notify=False)
try:
client = RtuClient(
payload["port"],
BAUD_RATE,
payload["slave"],
SERIAL_TIMEOUT_SECONDS,
)
client.__enter__()
self.client = client
self.next_status_poll = 0.0
self._emit(
"connection",
connected=True,
port=payload["port"],
slave=payload["slave"],
)
self._read_configuration()
except (OSError, serial.SerialException) as error:
self._handle_connection_error("连接", error)
except (TestFailure, ModbusException, RuntimeError) as error:
self._disconnect(notify=True, reason=str(error))
self._emit(
"error", operation="读取参数", message=str(error), modal=True
)
return

if command == "disconnect":
self._disconnect(notify=True)
return

try:
client = self._require_client()
if command == "read_configuration":
self._read_configuration()
self._emit("operation", message="参数读取完成")
elif command == "write_common":
client.write_multiple(CONFIG_BASE, payload["words"])
common = client.read_holding(CONFIG_BASE, COMMON_WORDS)
self._emit("common", words=common)
self._emit("operation", message="公共参数写入并回读完成")
elif command == "write_segments":
for index, words in enumerate(payload["segments"]):
address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
client.write_multiple(address, words)
segments = []
for index in range(SEGMENT_COUNT):
address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
segments.append(client.read_holding(address, SEGMENT_WORDS))
self._emit("segments", words=segments)
self._emit("operation", message="10 段参数写入并回读完成")
elif command == "control":
client.write_single(CONTROL, payload["value"])
self._emit("operation", message=payload["message"])
else:
raise RuntimeError("未知后台命令: %s" % command)
except (OSError, serial.SerialException) as error:
self._handle_connection_error(payload.get("operation", "通信"), error)
except (TestFailure, ModbusException, RuntimeError) as error:
self._emit(
"error",
operation=payload.get("operation", "通信"),
message=str(error),
modal=True,
)

def _poll_status(self):
try:
status = read_status(self._require_client())
self._emit("status", status=status)
self.next_status_poll = time.monotonic() + STATUS_POLL_SECONDS
except (OSError, serial.SerialException) as error:
self._handle_connection_error("状态轮询", error)
except (TestFailure, ModbusException, RuntimeError) as error:
self._emit(
"error", operation="状态轮询", message=str(error), modal=False
)
self.next_status_poll = time.monotonic() + 1.0

def run(self):
try:
while not self.stop_event.is_set():
try:
command, payload = self.commands.get(timeout=0.05)
except queue.Empty:
command = None
payload = None

if command == "shutdown":
break
if command is not None:
self._handle_command(command, payload)

if (
self.client is not None
and time.monotonic() >= self.next_status_poll
):
self._poll_status()
finally:
self._disconnect(notify=False)


class PlsrControlPanel:
def __init__(self, root):
self.root = root
self.root.title("PLSR 控制面板")
self.root.geometry("1180x720")
self.root.minsize(980, 650)
self.root.protocol("WM_DELETE_WINDOW", self._on_close)

self.connected = False
self.operation_pending = False
self.worker = SerialWorker()
self.worker.start()

self.port_var = tk.StringVar()
self.slave_var = tk.StringVar(value="1")
self.connection_var = tk.StringVar(value="未连接")
self.footer_var = tk.StringVar(value="请选择串口并连接")
self.status_vars = {
"position": tk.StringVar(value="--"),
"frequency": tk.StringVar(value="--"),
"state": tk.StringVar(value="--"),
"segment": tk.StringVar(value="--"),
"error": tk.StringVar(value="--"),
}
self.common_vars = {}
self.segment_vars = []
self.connection_widgets = []
self.action_buttons = []

self._configure_style()
self._build_ui()
self.refresh_ports()
self._set_connected(False)
self.root.after(50, self._drain_results)

def _configure_style(self):
default_font = tkfont.nametofont("TkDefaultFont")
default_font.configure(family="Microsoft YaHei UI", size=10)
text_font = tkfont.nametofont("TkTextFont")
text_font.configure(family="Microsoft YaHei UI", size=10)
style = ttk.Style(self.root)
style.configure("Connected.TLabel", foreground="#18794e")
style.configure("Disconnected.TLabel", foreground="#5f6368")
style.configure("Error.TLabel", foreground="#b42318")
style.configure("StatusValue.TLabel", font=("Microsoft YaHei UI", 11, "bold"))

def _build_ui(self):
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(2, weight=1)

connection = ttk.LabelFrame(self.root, text="连接")
connection.grid(row=0, column=0, padx=10, pady=(10, 6), sticky="ew")
connection.columnconfigure(7, weight=1)

ttk.Label(connection, text="串口").grid(row=0, column=0, padx=(8, 4), pady=8)
self.port_box = ttk.Combobox(
connection, textvariable=self.port_var, width=13, state="readonly"
)
self.port_box.grid(row=0, column=1, padx=4, pady=8)
refresh_button = ttk.Button(connection, text="刷新", command=self.refresh_ports)
refresh_button.grid(row=0, column=2, padx=(0, 12), pady=8)
ttk.Label(connection, text="串口参数").grid(row=0, column=3, padx=4, pady=8)
ttk.Label(connection, text="9600, 8E1").grid(row=0, column=4, padx=(4, 12), pady=8)
ttk.Label(connection, text="从站地址").grid(row=0, column=5, padx=4, pady=8)
slave_entry = ttk.Entry(connection, textvariable=self.slave_var, width=7)
slave_entry.grid(row=0, column=6, padx=(4, 12), pady=8)
self.connect_button = ttk.Button(
connection, text="连接", command=self._toggle_connection
)
self.connect_button.grid(row=0, column=8, padx=8, pady=8)
self.connection_label = ttk.Label(
connection,
textvariable=self.connection_var,
style="Disconnected.TLabel",
width=28,
anchor="e",
)
self.connection_label.grid(row=0, column=7, padx=8, pady=8, sticky="e")
self.connection_widgets = [self.port_box, refresh_button, slave_entry]

self._build_runtime_status()

notebook = ttk.Notebook(self.root)
notebook.grid(row=2, column=0, padx=10, pady=6, sticky="nsew")
common_tab = ttk.Frame(notebook, padding=12)
segment_tab = ttk.Frame(notebook, padding=12)
notebook.add(common_tab, text="公共参数 0x1000-0x1013")
notebook.add(segment_tab, text="10 段参数 0x1100-0x1197")
self._build_common_tab(common_tab)
self._build_segment_tab(segment_tab)

footer = ttk.Frame(self.root)
footer.grid(row=3, column=0, padx=10, pady=(4, 10), sticky="ew")
footer.columnconfigure(4, weight=1)
read_button = ttk.Button(footer, text="读取全部参数", command=self._read_all)
read_button.grid(row=0, column=0, padx=(0, 6))
common_button = ttk.Button(
footer, text="写入公共参数", command=self._write_common
)
common_button.grid(row=0, column=1, padx=6)
segment_button = ttk.Button(
footer, text="写入 10 段参数", command=self._write_segments
)
segment_button.grid(row=0, column=2, padx=6)
self.action_buttons.extend([read_button, common_button, segment_button])
self.footer_label = ttk.Label(
footer, textvariable=self.footer_var, anchor="e"
)
self.footer_label.grid(row=0, column=4, padx=(12, 0), sticky="ew")

def _build_runtime_status(self):
frame = ttk.LabelFrame(self.root, text="实时状态 0x2000-0x2006")
frame.grid(row=1, column=0, padx=10, pady=6, sticky="ew")
for column in range(11):
frame.columnconfigure(column, weight=1 if column % 2 else 0)

fields = (
("累计位置", "position"),
("当前频率", "frequency"),
("运行状态", "state"),
("当前段", "segment"),
("错误码", "error"),
)
for index, (label, key) in enumerate(fields):
ttk.Label(frame, text=label).grid(
row=0, column=index * 2, padx=(8, 4), pady=9, sticky="e"
)
ttk.Label(
frame, textvariable=self.status_vars[key], style="StatusValue.TLabel"
).grid(row=0, column=index * 2 + 1, padx=(4, 12), pady=9, sticky="w")

commands = ttk.Frame(frame)
commands.grid(row=0, column=10, padx=8, pady=6, sticky="e")
start_button = ttk.Button(
commands, text="启动", command=lambda: self._send_control(COMMAND_START)
)
stop_button = ttk.Button(
commands, text="停止", command=lambda: self._send_control(COMMAND_STOP)
)
clear_button = ttk.Button(commands, text="清零", command=self._clear_position)
start_button.grid(row=0, column=0, padx=3)
stop_button.grid(row=0, column=1, padx=3)
clear_button.grid(row=0, column=2, padx=3)
self.action_buttons.extend([start_button, stop_button, clear_button])

def _add_entry(self, parent, row, group, key, label, default="0", width=13):
column = group * 2
ttk.Label(parent, text=label).grid(
row=row, column=column, padx=(8, 4), pady=7, sticky="e"
)
variable = tk.StringVar(value=default)
ttk.Entry(parent, textvariable=variable, width=width).grid(
row=row, column=column + 1, padx=(4, 18), pady=7, sticky="w"
)
self.common_vars[key] = variable

def _add_combo(self, parent, row, group, key, label, options):
column = group * 2
ttk.Label(parent, text=label).grid(
row=row, column=column, padx=(8, 4), pady=7, sticky="e"
)
variable = tk.StringVar(value=options[0][0])
ttk.Combobox(
parent,
textvariable=variable,
values=option_labels(options),
state="readonly",
width=18,
).grid(row=row, column=column + 1, padx=(4, 18), pady=7, sticky="w")
self.common_vars[key] = variable

def _build_common_tab(self, parent):
for column in (1, 3, 5):
parent.columnconfigure(column, weight=1)

self._add_combo(
parent, 0, 0, "pulse_output", "脉冲输出(单逻辑 PLSR)", PULSE_OPTIONS
)
self._add_combo(
parent, 0, 1, "direction_output", "方向输出", DIRECTION_OPTIONS
)
self._add_entry(parent, 0, 2, "direction_delay", "方向延时 ms")

self._add_combo(parent, 1, 0, "wait_input", "WAIT 输入", INPUT_OPTIONS)
self._add_combo(parent, 1, 1, "ext_input", "EXT 输入", INPUT_OPTIONS)
self._add_combo(parent, 1, 2, "send_mode", "发送模式", SEND_OPTIONS)

self._add_combo(parent, 2, 0, "negative_logic", "方向逻辑", LOGIC_OPTIONS)
self._add_combo(parent, 2, 1, "curve_mode", "曲线模式", CURVE_OPTIONS)
self._add_combo(parent, 2, 2, "position_mode", "位置模式", POSITION_OPTIONS)

self._add_entry(parent, 3, 0, "segment_count", "段数", default="1")
self._add_entry(parent, 3, 1, "start_segment", "起始段", default="1")
self._add_entry(parent, 3, 2, "default_speed", "基准速度 Hz", default="1000")

self._add_entry(parent, 4, 0, "start_speed", "启动速度 Hz", default="100")
self._add_entry(parent, 4, 1, "stop_speed", "停止速度 Hz", default="100")
self._add_entry(parent, 4, 2, "acceleration", "加速时间 ms")

self._add_entry(parent, 5, 0, "deceleration", "减速时间 ms")

def _build_segment_tab(self, parent):
headers = (
("段 / 基址", 15),
("频率 Hz", 13),
("脉冲数", 15),
("等待类型", 19),
("WAIT ms", 11),
("ACT ms", 11),
("跳转段", 9),
)
for column, (label, _width) in enumerate(headers):
parent.columnconfigure(column, weight=1 if column in (1, 2, 3) else 0)
ttk.Label(parent, text=label, anchor="center").grid(
row=0, column=column, padx=4, pady=(0, 6), sticky="ew"
)

for index in range(SEGMENT_COUNT):
address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
ttk.Label(parent, text="%d / 0x%04X" % (index + 1, address)).grid(
row=index + 1, column=0, padx=(2, 8), pady=4, sticky="e"
)
variables = {
"frequency": tk.StringVar(value="1000"),
"pulses": tk.StringVar(value="0"),
"wait_type": tk.StringVar(value=WAIT_OPTIONS[0][0]),
"wait_ms": tk.StringVar(value="0"),
"act_ms": tk.StringVar(value="0"),
"jump": tk.StringVar(value="0"),
}
ttk.Entry(parent, textvariable=variables["frequency"], width=13).grid(
row=index + 1, column=1, padx=4, pady=4, sticky="ew"
)
ttk.Entry(parent, textvariable=variables["pulses"], width=15).grid(
row=index + 1, column=2, padx=4, pady=4, sticky="ew"
)
ttk.Combobox(
parent,
textvariable=variables["wait_type"],
values=option_labels(WAIT_OPTIONS),
state="readonly",
width=19,
).grid(row=index + 1, column=3, padx=4, pady=4, sticky="ew")
ttk.Entry(parent, textvariable=variables["wait_ms"], width=11).grid(
row=index + 1, column=4, padx=4, pady=4
)
ttk.Entry(parent, textvariable=variables["act_ms"], width=11).grid(
row=index + 1, column=5, padx=4, pady=4
)
ttk.Entry(parent, textvariable=variables["jump"], width=9).grid(
row=index + 1, column=6, padx=4, pady=4
)
self.segment_vars.append(variables)

def refresh_ports(self):
ports = [item.device for item in list_ports.comports()]
self.port_box["values"] = ports
if ports and self.port_var.get() not in ports:
self.port_var.set(ports[0])
elif not ports:
self.port_var.set("")

def _set_connected(self, connected, description=""):
self.connected = connected
self.connect_button.configure(
text="断开" if connected else "连接", state="normal"
)
connection_state = "disabled" if connected else "normal"
for widget in self.connection_widgets:
if widget is self.port_box:
widget.configure(state="disabled" if connected else "readonly")
else:
widget.configure(state=connection_state)
if connected:
self.connection_var.set(description or "已连接")
self.connection_label.configure(style="Connected.TLabel")
else:
self.connection_var.set(description or "未连接")
self.connection_label.configure(
style="Error.TLabel" if description else "Disconnected.TLabel"
)
for variable in self.status_vars.values():
variable.set("--")
self._update_action_state()

def _update_action_state(self):
state = "normal" if self.connected and not self.operation_pending else "disabled"
for button in self.action_buttons:
button.configure(state=state)

def _start_operation(self, message):
if not self.connected:
messagebox.showerror("未连接", "请先连接 PLSR 从站")
return False
if self.operation_pending:
return False
self.operation_pending = True
self.footer_var.set(message)
self._update_action_state()
return True

def _finish_operation(self, message):
self.operation_pending = False
self.footer_var.set(message)
self.footer_label.configure(style="TLabel")
self._update_action_state()

def _toggle_connection(self):
if self.connected:
self.operation_pending = False
self.footer_var.set("正在断开")
self.worker.submit("disconnect")
return
port = self.port_var.get().strip()
if not port:
messagebox.showerror("连接参数", "请选择可用串口")
return
try:
slave = parse_integer(self.slave_var.get(), "从站地址", 1, 247)
except ValueError as error:
messagebox.showerror("连接参数", str(error))
return
self.connect_button.configure(state="disabled")
self.operation_pending = True
self._update_action_state()
self.connection_var.set("正在连接 %s" % port)
self.footer_var.set("正在打开串口并读取参数")
self.worker.submit("connect", port=port, slave=slave)

def _common_words(self):
values = self.common_vars
segment_count = parse_integer(values["segment_count"].get(), "段数", 1, 10)
start_segment = parse_integer(
values["start_segment"].get(), "起始段", 1, 10
)
if start_segment > segment_count:
raise ValueError("起始段不能大于段数")

words = [0] * COMMON_WORDS
words[0] = option_value(
PULSE_OPTIONS, values["pulse_output"].get(), "脉冲输出"
)
words[1] = option_value(
DIRECTION_OPTIONS, values["direction_output"].get(), "方向输出"
)
words[2] = option_value(INPUT_OPTIONS, values["wait_input"].get(), "WAIT 输入")
words[3] = option_value(INPUT_OPTIONS, values["ext_input"].get(), "EXT 输入")
words[4] = option_value(SEND_OPTIONS, values["send_mode"].get(), "发送模式")
words[5] = parse_integer(
values["direction_delay"].get(), "方向延时", 0, 0xFFFF
)
words[6] = option_value(
LOGIC_OPTIONS, values["negative_logic"].get(), "方向逻辑"
)
words[7] = option_value(CURVE_OPTIONS, values["curve_mode"].get(), "曲线模式")
words[8] = option_value(
POSITION_OPTIONS, values["position_mode"].get(), "位置模式"
)
words[9] = segment_count
words[10] = start_segment
words[11:13] = split_u32(
parse_integer(
values["default_speed"].get(), "基准速度", 1, MAX_FREQUENCY_HZ
)
)
words[13:15] = split_u32(
parse_integer(
values["start_speed"].get(), "启动速度", 0, MAX_FREQUENCY_HZ
)
)
words[15] = 0
words[16:18] = split_u32(
parse_integer(
values["stop_speed"].get(), "停止速度", 0, MAX_FREQUENCY_HZ
)
)
words[18] = parse_integer(values["acceleration"].get(), "加速时间", 0, 0xFFFF)
words[19] = parse_integer(values["deceleration"].get(), "减速时间", 0, 0xFFFF)
return words

def _segment_words(self):
segment_count = parse_integer(
self.common_vars["segment_count"].get(), "段数", 1, SEGMENT_COUNT
)
encoded = []
for index, variables in enumerate(self.segment_vars):
name = "第 %d 段" % (index + 1)
frequency = parse_integer(
variables["frequency"].get(), name + "频率", 1, MAX_FREQUENCY_HZ
)
pulses = parse_integer(
variables["pulses"].get(), name + "脉冲数", -(1 << 31), (1 << 31) - 1
)
wait_type = option_value(
WAIT_OPTIONS, variables["wait_type"].get(), name + "等待类型"
)
wait_ms = parse_integer(
variables["wait_ms"].get(), name + " WAIT 时间", 0, 0xFFFF
)
act_ms = parse_integer(
variables["act_ms"].get(), name + " ACT 时间", 0, 0xFFFF
)
jump = parse_integer(variables["jump"].get(), name + "跳转段", 0, 10)
if index < segment_count and jump > segment_count:
raise ValueError(
"%s跳转段必须为 0 或不大于脉冲总段数 %d"
% (name, segment_count)
)
words = split_u32(frequency) + split_i32(pulses)
words.extend((wait_type, wait_ms, act_ms, jump))
encoded.append(words)
return encoded

def _set_common_words(self, words):
if len(words) != COMMON_WORDS:
raise ValueError("公共参数回读长度错误")
values = self.common_vars
values["pulse_output"].set(option_label(PULSE_OPTIONS, words[0]))
values["direction_output"].set(option_label(DIRECTION_OPTIONS, words[1]))
values["wait_input"].set(option_label(INPUT_OPTIONS, words[2]))
values["ext_input"].set(option_label(INPUT_OPTIONS, words[3]))
values["send_mode"].set(option_label(SEND_OPTIONS, words[4]))
values["direction_delay"].set(str(words[5]))
values["negative_logic"].set(option_label(LOGIC_OPTIONS, words[6]))
values["curve_mode"].set(option_label(CURVE_OPTIONS, words[7]))
values["position_mode"].set(option_label(POSITION_OPTIONS, words[8]))
values["segment_count"].set(str(words[9]))
values["start_segment"].set(str(words[10]))
values["default_speed"].set(str(words[11] | (words[12] << 16)))
values["start_speed"].set(str(words[13] | (words[14] << 16)))
values["stop_speed"].set(str(words[16] | (words[17] << 16)))
values["acceleration"].set(str(words[18]))
values["deceleration"].set(str(words[19]))

def _set_segment_words(self, segments):
if len(segments) != SEGMENT_COUNT:
raise ValueError("段参数回读数量错误")
for index, words in enumerate(segments):
if len(words) != SEGMENT_WORDS:
raise ValueError("第 %d 段回读长度错误" % (index + 1))
variables = self.segment_vars[index]
frequency = words[0] | (words[1] << 16)
pulse_bits = words[2] | (words[3] << 16)
pulses = pulse_bits - (1 << 32) if pulse_bits & 0x80000000 else pulse_bits
variables["frequency"].set(str(frequency))
variables["pulses"].set(str(pulses))
variables["wait_type"].set(option_label(WAIT_OPTIONS, words[4]))
variables["wait_ms"].set(str(words[5]))
variables["act_ms"].set(str(words[6]))
variables["jump"].set(str(words[7]))

def _read_all(self):
if self._start_operation("正在读取公共参数和 10 段参数"):
self.worker.submit("read_configuration", operation="读取参数")

def _write_common(self):
try:
words = self._common_words()
except ValueError as error:
messagebox.showerror("参数错误", str(error))
return
if self._start_operation("正在写入公共参数"):
self.worker.submit(
"write_common", words=words, operation="写入公共参数"
)

def _write_segments(self):
try:
segments = self._segment_words()
except ValueError as error:
messagebox.showerror("参数错误", str(error))
return
if self._start_operation("正在写入 10 段参数"):
self.worker.submit(
"write_segments", segments=segments, operation="写入段参数"
)

def _send_control(self, command):
messages = {
COMMAND_START: "启动命令已发送",
COMMAND_STOP: "停止命令已发送",
COMMAND_CLEAR: "清零命令已发送",
}
if self._start_operation("正在发送控制命令"):
self.worker.submit(
"control",
value=command,
message=messages[command],
operation="控制命令",
)

def _clear_position(self):
if messagebox.askyesno("确认清零", "确认将累计位置清零?"):
self._send_control(COMMAND_CLEAR)

def _show_status(self, status):
self.status_vars["position"].set(str(status.position))
self.status_vars["frequency"].set("%d Hz" % status.frequency_hz)
self.status_vars["state"].set(
"%s (%d)" % (STATUS_NAMES.get(status.state, "未知"), status.state)
)
self.status_vars["segment"].set(str(status.segment))
self.status_vars["error"].set(
"%s (%d)" % (ERROR_NAMES.get(status.error, "未知"), status.error)
)

def _handle_result(self, event, payload):
if event == "connection":
if payload["connected"]:
description = "%s / 从站 %d" % (payload["port"], payload["slave"])
self._set_connected(True, description)
self.footer_var.set("已连接,正在读取参数")
else:
reason = payload.get("reason", "")
self.operation_pending = False
self.connect_button.configure(state="normal")
self._set_connected(False, reason)
self.footer_var.set("连接已断开" if not reason else "通信错误")
elif event == "configuration":
self._set_common_words(payload["common"])
self._set_segment_words(payload["segments"])
self._finish_operation("全部参数已读取")
elif event == "common":
self._set_common_words(payload["words"])
elif event == "segments":
self._set_segment_words(payload["words"])
elif event == "status":
self._show_status(payload["status"])
elif event == "operation":
self._finish_operation(payload["message"])
elif event == "error":
message = "%s失败:%s" % (payload["operation"], payload["message"])
self._finish_operation(message)
self.footer_label.configure(style="Error.TLabel")
if payload.get("modal", False):
messagebox.showerror("PLSR 通信错误", message)

def _drain_results(self):
try:
while True:
event, payload = self.worker.results.get_nowait()
try:
self._handle_result(event, payload)
except (KeyError, ValueError) as error:
self._finish_operation("界面数据错误:%s" % error)
except queue.Empty:
pass
self.root.after(50, self._drain_results)

def _on_close(self):
self.worker.close()
self.root.destroy()


class _SelfTestVariable:
def __init__(self, value):
self.value = str(value)

def get(self):
return self.value

def set(self, value):
self.value = str(value)


def self_test():
assert BAUD_RATE == 9600
assert INPUT_OPTIONS == (("X4 (0)", 0), ("X5 (1)", 1))
assert [SEGMENT_1_BASE + index * SEGMENT_STRIDE
for index in range(SEGMENT_COUNT)] == [
0x1100, 0x1110, 0x1120, 0x1130, 0x1140,
0x1150, 0x1160, 0x1170, 0x1180, 0x1190,
]
assert parse_integer("0x10", "测试值", 0, 16) == 16
try:
parse_integer("17", "测试值", 0, 16)
except ValueError:
pass
else:
raise AssertionError("range validation did not reject 17")

panel = PlsrControlPanel.__new__(PlsrControlPanel)
panel.common_vars = {
"pulse_output": _SelfTestVariable(PULSE_OPTIONS[3][0]),
"direction_output": _SelfTestVariable(DIRECTION_OPTIONS[3][0]),
"wait_input": _SelfTestVariable(INPUT_OPTIONS[1][0]),
"ext_input": _SelfTestVariable(INPUT_OPTIONS[0][0]),
"send_mode": _SelfTestVariable(SEND_OPTIONS[1][0]),
"direction_delay": _SelfTestVariable(25),
"negative_logic": _SelfTestVariable(LOGIC_OPTIONS[1][0]),
"curve_mode": _SelfTestVariable(CURVE_OPTIONS[2][0]),
"position_mode": _SelfTestVariable(POSITION_OPTIONS[1][0]),
"segment_count": _SelfTestVariable(10),
"start_segment": _SelfTestVariable(2),
"default_speed": _SelfTestVariable(100000),
"start_speed": _SelfTestVariable(0),
"stop_speed": _SelfTestVariable(65536),
"acceleration": _SelfTestVariable(1000),
"deceleration": _SelfTestVariable(2000),
}
common = panel._common_words()
assert common[:11] == [3, 3, 1, 0, 1, 25, 1, 2, 1, 10, 2]
assert common[11:20] == [34464, 1, 0, 0, 0, 0, 1, 1000, 2000]

panel.segment_vars = []
for index in range(SEGMENT_COUNT):
panel.segment_vars.append({
"frequency": _SelfTestVariable(1000 + index),
"pulses": _SelfTestVariable(-1 if index == 0 else index),
"wait_type": _SelfTestVariable(WAIT_OPTIONS[4][0]),
"wait_ms": _SelfTestVariable(index),
"act_ms": _SelfTestVariable(index + 1),
"jump": _SelfTestVariable(0),
})
segments = panel._segment_words()
assert len(segments) == SEGMENT_COUNT
assert all(len(words) == SEGMENT_WORDS for words in segments)
assert segments[0] == [1000, 0, 0xFFFF, 0xFFFF, 4, 0, 1, 0]

panel.common_vars["segment_count"].set(1)
panel.segment_vars[0]["jump"].set(2)
try:
panel._segment_words()
except ValueError:
pass
else:
raise AssertionError("active segment jump exceeded segment count")
print("PLSR control panel self-test passed; no GUI or serial port was opened")


def parse_arguments(arguments=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--self-test",
action="store_true",
help="validate register encoding without opening the GUI or serial port",
)
return parser.parse_args(arguments)


def main(arguments=None):
args = parse_arguments(arguments)
if args.self_test:
self_test()
return 0
root = tk.Tk()
PlsrControlPanel(root)
root.mainloop()
return 0


if __name__ == "__main__":
raise SystemExit(main())

+ 1162
- 0
HostComputer/plsr_modbus_product_test.py
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 3
- 0
Middlewares/Third_Party/Micrium/Config/app_cfg.h Просмотреть файл

@@ -15,4 +15,7 @@
#define APP_TASK_START_STK_SIZE 256u #define APP_TASK_START_STK_SIZE 256u
#define APP_TASK_TEST_STK_SIZE 256u #define APP_TASK_TEST_STK_SIZE 256u


/* The short-profile pulse IRQ needs more than the port's 128-word default. */
#define OS_CPU_EXCEPT_STK_SIZE 256u

#endif #endif

+ 2
- 128
Modbus/Inc/modbus_rtu_slave.h Просмотреть файл

@@ -3,145 +3,19 @@


#include "stm32f4xx_hal.h" #include "stm32f4xx_hal.h"
#include <stdint.h> #include <stdint.h>

#ifdef __cplusplus #ifdef __cplusplus
extern "C"
{
extern "C" {
#endif #endif


/**
* @brief Modbus 数据模型的最大地址
*
* 任务要求规定线圈和保持寄存器地址范围均为 0~0x270F,
* 因此每种数据共有 10000 项
*/
#define MODBUS_MAP_LAST_ADDRESS (0x270FU) // 保持寄存器或线圈地址
#define MODBUS_MAP_ITEM_COUNT (0x2710U) // 保持寄存器或线圈数量

#define MODBUS_SLAVE_DEFAULT_ADDRESS (1U) #define MODBUS_SLAVE_DEFAULT_ADDRESS (1U)
#define MODBUS_CONNECTION_TIMEOUT_MS (1000U)

/**
* @brief 与 TouchWin 触摸屏联调使用的演示地址
*
* 可以直接访问以下地址
*/
#define HMI_REG_DEVICE_ID (0U)
#define HMI_REG_UPTIME_SECONDS (1U)
#define HMI_REG_RX_FRAME_COUNT (2U)
#define D3 (3U)
#define D4 (4U)
#define D5 (5U)
#define D6 (6U)
#define D7 (7U)
#define HMI_REG_WRITE_TEST (10U)
#define HMI_COIL_WRITE_TEST (0U)

#define MODBUS_RETAINED_D_START (100U)
#define MODBUS_RETAINED_D_END (120U)
#define MODBUS_RETAINED_D_COUNT (21U)

#define MODBUS_BACKUP_MAGIC (0x44313030UL)


/**
* @brief 掉电保持寄存器在Backup SRAM中的存储格式
*/
typedef struct
{
uint32_t magic; ///< 掉电保持数据有效标志
uint16_t retainedD[MODBUS_RETAINED_D_COUNT]; ///< D100~D120保持值
} MODBUS_BACKUP_DATA;
/**
* @brief Modbus 从站通信统计信息
*/
typedef struct
{
uint32_t rxEventCount; ///< 串口接收事件总数
uint32_t validFrameCount; ///< CRC 和从站地址均有效的帧数
uint32_t txFrameCount; ///< 成功启动 DMA 发送的帧数
uint32_t crcErrorCount; ///< CRC 校验错误帧数
uint32_t ignoredAddressCount; ///< 因从站地址不匹配而忽略的帧数
uint32_t illegalFunctionCount; ///< 非法功能码计数
uint32_t illegalAddressCount; ///< 非法数据地址计数
uint32_t illegalValueCount; ///< 非法数据值计数
uint32_t droppedFrameCount; ///< 接收槽占用或长度错误导致的丢帧数
uint32_t uartErrorCount; ///< HAL 串口错误计数
} MODBUS_SLAVE_STATS;
/** @brief Modbus 从站通信统计数据 */
extern volatile MODBUS_SLAVE_STATS ModbusSlaveStatistics;
/**
* @brief 初始化 Modbus 从站并启动 USART DMA 空闲接收
* @param[in] huart 已由 HAL 初始化完成的串口句柄
* @param[in] slaveAddress 从站地址,有效范围为 1~247
* @retval HAL_OK 初始化成功且 DMA 接收已启动
* @retval HAL_ERROR 输入参数无效
* @retval HAL_BUSY 串口或 DMA 当前忙
*/
HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart,
uint8_t slaveAddress); uint8_t slaveAddress);
/**
* @brief 校验并处理一帧待处理请求
*
* 本函数应在 uC/OS-II 任务中周期调用中断只负责接收数据,
* CRC 校验、协议解析和响应发送均在任务上下文中执行
*/
void ModbusSlavePoll(void); void ModbusSlavePoll(void);
/**
* @brief 处理 HAL 串口 DMA 接收事件
* @param[in] huart 产生接收事件的串口句柄
* @param[in] size 本次接收到的字节数
*/
void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size); void ModbusSlaveOnRxEvent(UART_HandleTypeDef *huart, uint16_t size);
/**
* @brief 处理 HAL 串口 DMA 发送完成事件
* @param[in] huart 完成发送的串口句柄
*/
void ModbusSlaveOnTxComplete(UART_HandleTypeDef *huart); void ModbusSlaveOnTxComplete(UART_HandleTypeDef *huart);
/**
* @brief 处理 HAL 串口错误事件并恢复接收
* @param[in] huart 发生错误的串口句柄
*/
void ModbusSlaveOnUartError(UART_HandleTypeDef *huart); void ModbusSlaveOnUartError(UART_HandleTypeDef *huart);
/**
* @brief 写入一个保持寄存器
* @param[in] address 保持寄存器地址
* @param[in] value 待写入的 16 位数据
* @retval 1 写入成功
* @retval 0 保持寄存器地址无效
*/
uint8_t ModbusSlaveSetHoldingRegister(uint16_t address, uint16_t value);
/**
* @brief 读取一个保持寄存器
* @param[in] address 保持寄存器地址
* @param[out] value 用于保存读取结果的指针
* @retval 1 读取成功
* @retval 0 保持寄存器地址无效或输出指针为空
*/
uint8_t ModbusSlaveGetHoldingRegister(uint16_t address, uint16_t *value);
/**
* @brief 设置一个线圈的状态
* @param[in] address 线圈地址
* @param[in] state 0 表示复位,非 0 表示置位
* @retval 1 设置成功
* @retval 0 线圈地址无效
*/
uint8_t ModbusSlaveSetCoil(uint16_t address, uint8_t state);
/**
* @brief 读取一个线圈的状态
* @param[in] address 线圈地址
* @param[out] state 用于保存线圈状态的指针,结果为 0 或 1
* @retval 1 读取成功
* @retval 0 线圈地址无效或输出指针为空
*/
uint8_t ModbusSlaveGetCoil(uint16_t address, uint8_t *state);
/**
* @brief 判断指定时间内是否收到过有效请求
* @param[in] timeoutMs 连接超时时间,单位为毫秒
* @retval 1 从站在指定时间内收到过有效请求
* @retval 0 尚未收到有效请求或连接已经超时
*/
uint8_t ModbusSlaveIsConnected(uint32_t timeoutMs);
void ModbusRetainedRegistersLoad(void);
void ModbusRetainedRegistersPoll(void);


#ifdef __cplusplus #ifdef __cplusplus
} }


+ 256
- 777
Modbus/Src/modbus_rtu_slave.c
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 94
- 0
PLSR/Inc/plsr.h Просмотреть файл

@@ -0,0 +1,94 @@
#ifndef PLSR_H
#define PLSR_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

#define PLSR_CONFIG_FIRST_ADDRESS (0x1000U)
#define PLSR_CONFIG_LAST_ADDRESS (0x1197U)
#define PLSR_STATUS_FIRST_ADDRESS (0x2000U)
#define PLSR_STATUS_LAST_ADDRESS (0x2006U)
#define PLSR_CONTROL_ADDRESS (0x3000U)

typedef enum
{
PLSR_MB_NOT_HANDLED = 0,
PLSR_MB_OK,
PLSR_MB_ILLEGAL_ADDRESS,
PLSR_MB_ILLEGAL_VALUE,
PLSR_MB_DEVICE_BUSY,
PLSR_MB_SERVER_FAILURE
} PLSR_MB_RESULT;

typedef enum
{
PLSR_STATUS_UNINITIALIZED = 0,
PLSR_STATUS_IDLE = 1,
PLSR_STATUS_ACCELERATING = 2,
PLSR_STATUS_RUNNING = 3,
PLSR_STATUS_DECELERATING = 4,
PLSR_STATUS_WAITING = 5,
PLSR_STATUS_PAUSED = 6,
PLSR_STATUS_COMPLETED = 7,
PLSR_STATUS_STOPPED = 8,
PLSR_STATUS_ERROR = 9
} PLSR_STATUS;

typedef enum
{
PLSR_ERROR_NONE = 0,
PLSR_ERROR_INVALID_TRANSITION = 1,
PLSR_ERROR_RESOURCE_CONFLICT = 2,
PLSR_ERROR_INVALID_RESOURCE = 3,
PLSR_ERROR_TIMER = 4,
PLSR_ERROR_COUNT = 5,
PLSR_ERROR_POSITIVE_LIMIT = 6,
PLSR_ERROR_NEGATIVE_LIMIT = 7,
PLSR_ERROR_EMERGENCY_STOP = 8,
PLSR_ERROR_INTERNAL = 9
} PLSR_ERROR;

uint8_t PlsrInit(void);
void PlsrPoll1ms(void);

PLSR_MB_RESULT PlsrModbusReadHolding(uint16_t startAddress,
uint16_t quantity,
uint16_t *values);
PLSR_MB_RESULT PlsrModbusWriteHolding(uint16_t startAddress,
uint16_t quantity,
const uint16_t *values);

/* Called by the selected output timer update interrupt once per pulse. */
void PlsrPulseTimerIrq(uint8_t pulseOutput);

#ifdef PLSR_HOST_TEST
uint64_t PlsrTestDivideU64ByU32(uint64_t dividend,
uint32_t divisor,
uint32_t *remainder);
void PlsrTestSetInput(uint8_t inputSelection, uint8_t level);
void PlsrTestEmitPulses(uint32_t pulseCount);
void PlsrTestEmitPulseOnCriticalEntry(void);
void PlsrTestEmitPulseAfterCriticalEntries(uint8_t entriesToSkip);
void PlsrTestEmitPulseOnCriticalExit(void);
void PlsrTestLatchPulseOnCriticalEntry(void);
void PlsrTestServicePendingPulse(void);
void PlsrTestFailNextStart(void);
void PlsrTestFailNextFrequencyAtUpdate(void);
uint8_t PlsrTestPulseIsActive(void);
uint32_t PlsrTestOutputFrequency(void);
uint32_t PlsrTestQueuedFrequency(void);
uint8_t PlsrTestDirectionLevel(void);
void PlsrTestSetPosition(int32_t position, uint8_t positionValid);
void PlsrTestClearPersistentStorage(void);
void PlsrTestResetSaveCount(void);
uint32_t PlsrTestSaveCount(void);
#endif

#ifdef __cplusplus
}
#endif

#endif /* PLSR_H */

+ 3599
- 0
PLSR/Src/plsr.c
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 49
- 0
PLSR/Src/plsr_internal.h Просмотреть файл

@@ -0,0 +1,49 @@
#ifndef PLSR_INTERNAL_H
#define PLSR_INTERNAL_H

#include <stdint.h>

#define PLSR_SEGMENT_COUNT_MAX (10U)
#define PLSR_FREQUENCY_MAX_HZ (100000UL)

typedef struct
{
uint32_t frequencyHz;
int32_t pulses;
uint16_t waitType;
uint16_t waitTimeMs;
uint16_t actTimeMs;
uint16_t jumpSegment;
} PLSR_SEGMENT_CONFIG;

typedef struct
{
uint16_t pulseOutput;
uint16_t directionOutput;
uint16_t waitInput;
uint16_t extInput;
uint16_t sendMode;
uint16_t directionDelayMs;
uint16_t directionNegativeLogic;
uint16_t curveMode;
uint16_t positionMode;
uint16_t segmentCount;
uint16_t startSegment;
uint32_t defaultSpeedHz;
uint32_t startSpeedHz;
uint32_t stopSpeedHz;
uint16_t accelerationTimeMs;
uint16_t decelerationTimeMs;
PLSR_SEGMENT_CONFIG segments[PLSR_SEGMENT_COUNT_MAX];
} PLSR_CONFIG;

typedef struct
{
PLSR_CONFIG config;
int32_t position;
uint8_t positionValid;
uint8_t wasBusy;
uint16_t reserved;
} PLSR_PERSIST_PAYLOAD;

#endif /* PLSR_INTERNAL_H */

+ 34
- 0
PLSR/Src/plsr_platform.h Просмотреть файл

@@ -0,0 +1,34 @@
#ifndef PLSR_PLATFORM_H
#define PLSR_PLATFORM_H

#include "plsr_internal.h"
#include <stdint.h>

uint8_t PlsrPlatformInit(void);
uint8_t PlsrPlatformPrepare(uint8_t pulseOutput,
uint8_t directionOutput,
uint8_t directionLevel);
uint8_t PlsrPlatformStartPulse(uint8_t pulseOutput,
uint32_t firstFrequencyHz,
uint32_t queuedFrequencyHz,
uint32_t *actualFirstFrequencyHz,
uint32_t *actualQueuedFrequencyHz);
uint8_t PlsrPlatformQueueFrequency(uint8_t pulseOutput,
uint32_t frequencyHz,
uint32_t *actualFrequencyHz);
void PlsrPlatformDrainPendingPulse(uint8_t pulseOutput);
uint32_t PlsrPlatformActiveFrequency(uint8_t pulseOutput);
void PlsrPlatformStopPulse(uint8_t pulseOutput);
uint8_t PlsrPlatformReadInput(uint8_t inputSelection);

uint8_t PlsrPlatformLoad(PLSR_PERSIST_PAYLOAD *payload);
uint8_t PlsrPlatformSave(const PLSR_PERSIST_PAYLOAD *payload);
void PlsrPlatformCheckpointConfig(const PLSR_CONFIG *config);
void PlsrPlatformCheckpointPosition(int32_t position,
uint8_t positionValid,
uint8_t wasBusy);

uint32_t PlsrPlatformEnterCritical(void);
void PlsrPlatformExitCritical(uint32_t state);

#endif /* PLSR_PLATFORM_H */

+ 1202
- 0
PLSR/Src/plsr_platform_f407.c
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 42
- 0
tests/plsr_host/run_tests.ps1 Просмотреть файл

@@ -0,0 +1,42 @@
$ErrorActionPreference = "Stop"

$gcc = "D:\Dev-Cpp\MinGW64\bin\gcc.exe"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$temporaryExe = Join-Path ([System.IO.Path]::GetTempPath()) `
("plsr_host_tests_{0}.exe" -f [System.Guid]::NewGuid().ToString("N"))
$exitCode = 1

if (-not (Test-Path -LiteralPath $gcc)) {
throw "Required compiler not found: $gcc"
}

$compileArguments = @(
"-std=c99"
"-Wall"
"-Wextra"
"-Werror"
"-DPLSR_HOST_TEST"
"-I$repoRoot\PLSR\Inc"
"-I$repoRoot\PLSR\Src"
"$repoRoot\PLSR\Src\plsr.c"
"$repoRoot\PLSR\Src\plsr_platform_f407.c"
"$PSScriptRoot\test_plsr_host.c"
"-o"
$temporaryExe
)

try {
& $gcc @compileArguments
if ($LASTEXITCODE -ne 0) {
$exitCode = $LASTEXITCODE
}
else {
& $temporaryExe
$exitCode = $LASTEXITCODE
}
}
finally {
Remove-Item -LiteralPath $temporaryExe -Force -ErrorAction SilentlyContinue
}

exit $exitCode

+ 1964
- 0
tests/plsr_host/test_plsr_host.c
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


Загрузка…
Отмена
Сохранить