feat: complete assembly mesh relay control path

- synchronize STM8 application and external flash layout updates\n- add assembly bootloader and assembly application test projects\n- add UART1 interrupt receive ring buffer\n- parse JDY-28M Mesh outer frames and source addresses\n- add CRC-16/MODBUS validation\n- implement relay control command 0x01 and response 0x81\n- support unicast and broadcast relay control frames\n- update host protocol, OTA documents, and serial tools
This commit is contained in:
十七
2026-11-05 16:12:51 +08:00
parent df1aad3d16
commit ebef6a8694
40 changed files with 2133 additions and 307 deletions
+33
View File
@@ -0,0 +1,33 @@
# STM8S003F3P6 汇编主程序实验工程。
# 主程序固定从0x8500开始链接,与汇编Bootloader的新地址布局匹配。
PROJECT := stm8_app_asm
BUILD_DIR := build
AS := sdasstm8
LD := sdldstm8
PACKIHX := packihx
ASMFLAGS := -plosgff
LINKFLAGS := -mjwxu -b HOME=0x8500 -b CODE=0x8568
OBJECT := $(BUILD_DIR)/app_vectors.rel
IHX := $(BUILD_DIR)/$(PROJECT).ihx
HEX := $(BUILD_DIR)/$(PROJECT).hex
.PHONY: all clean
all: $(HEX)
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(OBJECT): src/app_vectors.asm | $(BUILD_DIR)
$(AS) $(ASMFLAGS) -o $@ $<
$(IHX): $(OBJECT)
$(LD) $(LINKFLAGS) -i $@ $^
$(HEX): $(IHX)
$(PACKIHX) $< > $@
clean:
rm -rf $(BUILD_DIR)
+20
View File
@@ -0,0 +1,20 @@
# STM8 汇编主程序实验工程
本工程是正式 C 主程序的并行汇编实现,当前只验证最小启动链路:
1. 应用向量表从 `0x8500` 开始;
2. Bootloader 能够跳转到应用复位入口;
3. TIM4 中断能够从 Bootloader 的固定向量转发到应用程序;
4. PB5 运行灯每约500ms反转一次。
应用启动后将 `CLK_CKDIVR` 配置为 HSI/1,主频为16MHz;TIM4和UART1参数均按16MHz计算。
当前不包含蓝牙、UART、ADC、继电器和OTA功能。后续每增加一个模块,都应先独立编译并与Bootloader合并后再进行实物验证。
## 构建
```sh
make
```
输出文件:`build/stm8_app_asm.hex`。
+448
View File
@@ -0,0 +1,448 @@
;-------------------------------------------------------------------------------
; STM8S003F3P6 汇编主程序最小启动工程
;
; 应用程序从0x8500开始链接。STM8硬件中断仍然使用固定向量地址,实际产品
; 由Bootloader在0x8064等固定向量中转发到应用程序对应向量槽。
;-------------------------------------------------------------------------------
.module app_vectors
.optsdcc -mstm8
.area HOME (ABS)
; 每个中断向量项占4字节:0x82 + 3字节远跳转地址。
int _App_Reset
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
; IRQ18:UART1接收,进入汇编接收中断入口。
int _App_UART1Vector
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
int _App_DefaultInterrupt
; IRQ23:TIM4更新中断,Bootloader固定向量会转发到这里。
int _App_TIM4Vector
.area CODE
;-------------------------------------------------------------------------------
; 应用复位入口。
; 初始化堆栈、PB5和TIM4,然后进入主循环。Bootloader跳转前不依赖应用RAM状态,
; 因此这里主动设置堆栈,保证汇编主程序可以独立运行。
;-------------------------------------------------------------------------------
_App_Reset:
sim
ldw x, #0x03FF
ldw sp, x
; CLK_CKDIVR:HSI默认16MHz经/8得到约2MHz;写0后改为HSI/1=16MHz。
; 后续TIM4和UART1参数均按16MHz配置。
mov 0x50C6, #0x00
; PB5:推挽输出,初始输出低电平。当前硬件运行灯按低电平点亮验证。
bres 0x5005, #5
bset 0x5007, #5
bset 0x5008, #5
bres 0x5009, #5
; TIM4:16MHz/128/125,目标约1ms更新一次。
mov 0x5340, #0x00
mov 0x5343, #0x01
mov 0x5347, #0x07
mov 0x5348, #0x7C
mov 0x5346, #0x00
mov 0x5345, #0x01
mov 0x5344, #0x00
mov 0x5340, #0x01
; UART1:当前系统时钟16MHz,9600bps使用BRR1=0x68、BRR2=0x03。
; PD5为TX推挽输出,PD6为RX输入并开启内部上拉。
; 内部上拉用于保证串口线在适配器未驱动或连接接触不稳定时保持空闲高电平,
; 不改变正常UART接收的逻辑电平。
; UART1引脚配置:接收中断只保存一个字节,主循环负责发送回显。
bset 0x5011, #5
bset 0x5012, #5
bres 0x5011, #6
bset 0x5012, #6
mov 0x5235, #0x00
mov 0x5233, #0x03
mov 0x5232, #0x68
mov 0x5234, #0x00
mov 0x5236, #0x00
; 清理Bootloader或上电阶段可能遗留的UART状态:
; 先读SR,再读DR。这样可以清除RXNE及可能的帧错误、噪声错误、溢出错误,
; 防止刚开启接收中断时立即进入一次“伪接收”或把残留字节当作有效数据。
ld a, 0x5230
ld a, 0x5231
; UART接收环形缓冲区使用RAM 0x0020~0x002F,共16字节。
; 0x0030为写索引,0x0031为读索引,0x0032为溢出计数。
mov 0x0030, #0x00
mov 0x0031, #0x00
mov 0x0032, #0x00
; Mesh外层解析状态:0等待F1,1等待DD,2等待09,3跳过4字节地址,
; 4接收业务区直到0A。0x0035记录业务区长度,0x0036表示最近一帧有效。
mov 0x0034, #0x00
mov 0x0035, #0x00
mov 0x0036, #0x00
mov 0x5235, #0x2C
rim
; 发送一次固定的0x55,仅用于现场确认TX链路、波特率和串口接线。
; 该字节不是业务协议数据,正式联调确认TX正常后可删除这三行。
ld a, #0x55
ld 0x0020, a
_App_StartupTxWait:
ld a, 0x5230
and a, #0x80
jreq _App_StartupTxWait
ld a, 0x0020
ld 0x5231, a
_App_MainLoop:
; 主循环从环形缓冲区取出字节。UART中断只负责接收和入队,
; 当前阶段不再对任意字节原样回显;所有字节统一交给Mesh外层状态机。
ld a, 0x0030
cp a, 0x0031
jreq _App_MainLoop
ld a, 0x0031
add a, #0x20
ldw x, #0x0000
ld xl, a
ld a, (x)
ld 0x0020, a
ld a, 0x0031
inc a
and a, #0x0F
ld 0x0031, a
; 将取出的字节交给Mesh外层帧状态机。当前仅识别帧边界,
; 不执行继电器等业务动作。
call _App_MeshParseByte
ld a, 0x0036
jreq _App_MainLoop
call _App_ProcessBusiness
mov 0x0036, #0x00
jra _App_MainLoop
;-------------------------------------------------------------------------------
; JDY-28M Mesh外层帧状态机。
;
; 接收格式:F1 DD 09 | SRC_H SRC_L | DST_H DST_L | BUSINESS | 0A
; 当前阶段只确认固定头、跳过源/目标地址,并以0A作为外层帧结束符。
; 业务数据最大按20字节外层限制约束为12字节,超长帧直接重新寻找F1。
;-------------------------------------------------------------------------------
_App_MeshParseByte:
ld a, 0x0020
ld xl, a
ld a, 0x0034
cp a, #0x00
jreq _App_ParseWaitF1
cp a, #0x01
jreq _App_ParseWaitDD
cp a, #0x02
jreq _App_ParseWait09
cp a, #0x03
jreq _App_ParseAddress
; 状态4:业务数据区,0A表示完整外层帧结束。
cp a, #0x04
jreq _App_ParseBusiness
mov 0x0034, #0x00
ret
_App_ParseWaitF1:
ld a, 0x0020
cp a, #0xF1
jrne _App_ParseReturn
mov 0x0034, #0x01
ret
_App_ParseWaitDD:
ld a, 0x0020
cp a, #0xDD
jrne _App_ParseReset
mov 0x0034, #0x02
ret
_App_ParseWait09:
ld a, 0x0020
cp a, #0x09
jrne _App_ParseReset
mov 0x0034, #0x03
mov 0x0035, #0x00
ret
_App_ParseAddress:
; 保存外层源地址和目标地址:前2字节为源地址,后2字节为目标地址。
ld a, 0x0035
add a, #0x38
ldw x, #0x0000
ld xl, a
ld a, 0x0020
ld (x), a
inc 0x0035
ld a, 0x0035
cp a, #0x04
jrne _App_ParseReturn
mov 0x0034, #0x04
mov 0x0035, #0x00
ret
_App_ParseBusiness:
ld a, 0x0020
cp a, #0x0A
jreq _App_ParseFrameDone
; JDY-28M实测可能在Mesh业务数据后携带0D 0A,
; 0D属于外层结束符的一部分,不应计入业务长度和CRC数据。
cp a, #0x0D
jreq _App_ParseReturn
; 保存业务区字节,后续可从0x0040开始读取CMD、LEN、DATA和CRC16。
ld a, 0x0035
add a, #0x40
ldw x, #0x0000
ld xl, a
ld a, 0x0020
ld (x), a
inc 0x0035
ld a, 0x0035
cp a, #0x0C
jrult _App_ParseReturn
; 业务区超过外层长度限制,放弃当前帧。
mov 0x0034, #0x00
ret
_App_ParseFrameDone:
; 业务区至少包含CMD、LEN和CRC16,共4字节。
ld a, 0x0035
cp a, #0x04
jrult _App_ParseReset
; LEN必须等于业务区长度减去CMD、LEN和CRC16四个字节。
ld a, 0x0035
sub a, #0x04
ld 0x0037, a
ld a, 0x0041
cp a, 0x0037
jrne _App_ParseReset
; 记录一帧外层和业务长度均合法的Mesh数据。
mov 0x0036, #0x01
mov 0x0034, #0x00
ret
_App_ParseReset:
mov 0x0034, #0x00
_App_ParseReturn:
ret
;-------------------------------------------------------------------------------
; 处理当前已通过外层和长度检查的业务帧。
; 当前仅实现CMD=01继电器控制,CRC计算范围为业务区的CMD、LEN、DATA。
;-------------------------------------------------------------------------------
_App_ProcessBusiness:
; 先计算CMD、LEN、DATA的CRC,并与业务帧末尾两个字节比较;
; CRC错误时静默丢弃,绝不执行继电器动作。
call _App_CrcBusiness3
ld a, 0x003D
cp a, 0x0043
jreq _App_CrcHighOK
jpf _App_BusinessReturn
_App_CrcHighOK:
ld a, 0x003C
cp a, 0x0044
jreq _App_CrcLowOK
jpf _App_BusinessReturn
_App_CrcLowOK:
ld a, 0x0040
cp a, #0x01
jreq _App_CmdOK
jpf _App_BusinessReturn
_App_CmdOK:
ld a, 0x0041
cp a, #0x01
jreq _App_LenOK
jpf _App_BusinessReturn
_App_LenOK:
ld a, 0x0042
cp a, #0x01
jreq _App_RelayOn
cp a, #0x00
jreq _App_RelayOff
jpf _App_BusinessReturn
; 只有DATA=00或01允许控制继电器。
_App_RelayOff:
mov 0x5000, #0x00
jra _App_RelayRespond
_App_RelayOn:
mov 0x5000, #0x02
_App_RelayRespond:
; 业务区改写为CMD=81、LEN=1、DATA=当前控制状态。
mov 0x0040, #0x81
mov 0x0041, #0x01
ld a, 0x5000
and a, #0x02
jreq _App_ResponseOff
mov 0x0042, #0x01
jra _App_ResponseCrc
_App_ResponseOff:
mov 0x0042, #0x00
_App_ResponseCrc:
call _App_CrcBusiness3
ld a, 0x003D
ld 0x0043, a
ld a, 0x003C
ld 0x0044, a
; JDY-28M发送格式:41 54 2B AA A1 | 目标地址 | A0 | 业务区 | 0D 0A。
mov 0x003E, #0x41
call _App_SendByte
mov 0x003E, #0x54
call _App_SendByte
mov 0x003E, #0x2B
call _App_SendByte
mov 0x003E, #0xAA
call _App_SendByte
mov 0x003E, #0xA1
call _App_SendByte
ld a, 0x0038
ld 0x003E, a
call _App_SendByte
ld a, 0x0039
ld 0x003E, a
call _App_SendByte
mov 0x003E, #0xA0
call _App_SendByte
ld a, #0x00
ld 0x003F, a
_App_SendResponseBusiness:
ld a, 0x003F
add a, #0x40
ldw x, #0x0000
ld xl, a
ld a, (x)
ld 0x003E, a
call _App_SendByte
inc 0x003F
ld a, 0x003F
cp a, #0x05
jrne _App_SendResponseBusiness
; 按JDY-28M Mesh发送协议保留完整帧尾0D 0A。
mov 0x003E, #0x0D
call _App_SendByte
mov 0x003E, #0x0A
call _App_SendByte
_App_BusinessReturn:
ret
; CRC-16/MODBUS,计算业务区0x0040~0x0042,结果低字节在0x003C、高字节在0x003D。
_App_CrcBusiness3:
mov 0x003C, #0xFF
mov 0x003D, #0xFF
mov 0x003F, #0x00
_App_CrcByte:
ld a, 0x003F
add a, #0x40
ldw x, #0x0000
ld xl, a
ld a, (x)
xor a, 0x003C
ld 0x003C, a
mov 0x003E, #0x08
_App_CrcBit:
srl 0x003D
rrc 0x003C
jrnc _App_CrcNoPoly
ld a, 0x003C
xor a, #0x01
ld 0x003C, a
ld a, 0x003D
xor a, #0xA0
ld 0x003D, a
_App_CrcNoPoly:
dec 0x003E
jrne _App_CrcBit
inc 0x003F
ld a, 0x003F
cp a, #0x03
jrne _App_CrcByte
ret
_App_SendByte:
ld a, 0x5230
and a, #0x80
jreq _App_SendByte
ld a, 0x003E
ld 0x5231, a
ret
;-------------------------------------------------------------------------------
; TIM4中断服务函数。
; 每500次约1ms中断翻转一次PB5,因此目标周期约500ms。计数变量放在RAM中,
; 不使用栈上局部变量,便于后续继续控制RAM占用。
;-------------------------------------------------------------------------------
_App_TIM4Vector:
push a
pushw x
ld a, 0x5344
and a, #0x01
jreq _App_TIM4Exit
mov 0x5344, #0x00
ldw x, 0x0000
incw x
cpw x, #500
jrne _App_TIM4Save
ldw x, #0
bcpl 0x5005, #5
_App_TIM4Save:
ldw 0x0000, x
_App_TIM4Exit:
popw x
pop a
iret
; 未使用中断统一停机,防止错误向量执行未知代码。
_App_DefaultInterrupt:
sim
_App_DefaultLoop:
jra _App_DefaultLoop
; UART1接收中断:读取DR清除RXNE,将字节保存到单字节测试缓冲区。
_App_UART1Vector:
push a
pushw x
ld a, 0x5230
and a, #0x20
jreq _App_UART1Exit
ld a, 0x5231
ld 0x0033, a
ld a, 0x0030
inc a
and a, #0x0F
cp a, 0x0031
jreq _App_UART1Exit
ld a, 0x0030
add a, #0x20
ldw x, #0x0000
ld xl, a
ld a, 0x0033
ld (x), a
ld a, 0x0030
inc a
and a, #0x0F
ld 0x0030, a
_App_UART1Exit:
popw x
pop a
iret