Files
LFP_48V_mBMS_FW/generate_word_doc.py

589 lines
36 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
KT mBMS CAN 통신 프로토콜 규격서 Word(.docx) 생성 스크립트
"""
import os
import sys
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL
from docx.oxml import OxmlElement, parse_xml
from docx.oxml.ns import nsdecls, qn
def set_cell_background(cell, hex_color):
"""셀 배경색 지정"""
shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{hex_color}"/>')
cell._tc.get_or_add_tcPr().append(shading_elm)
def set_cell_margins(cell, top=100, bottom=100, left=150, right=150):
"""셀 여백 지정 (단위: dxa, 1pt = 20dxa)"""
tcPr = cell._tc.get_or_add_tcPr()
tcMar = parse_xml(
f'<w:tcMar {nsdecls("w")}>'
f'<w:top w:w="{top}" w:type="dxa"/>'
f'<w:bottom w:w="{bottom}" w:type="dxa"/>'
f'<w:left w:w="{left}" w:type="dxa"/>'
f'<w:right w:w="{right}" w:type="dxa"/>'
f'</w:tcMar>'
)
tcPr.append(tcMar)
def set_table_borders(table, color="D3D3D3", sz="4", val="single"):
"""테이블 전체 테두리 지정"""
tblPr = table._tbl.tblPr
borders = parse_xml(
f'<w:tblBorders {nsdecls("w")}>'
f'<w:top w:val="{val}" w:sz="{sz}" w:space="0" w:color="{color}"/>'
f'<w:left w:val="{val}" w:sz="{sz}" w:space="0" w:color="{color}"/>'
f'<w:bottom w:val="{val}" w:sz="{sz}" w:space="0" w:color="{color}"/>'
f'<w:right w:val="{val}" w:sz="{sz}" w:space="0" w:color="{color}"/>'
f'<w:insideH w:val="{val}" w:sz="{sz}" w:space="0" w:color="{color}"/>'
f'<w:insideV w:val="{val}" w:sz="{sz}" w:space="0" w:color="{color}"/>'
f'</w:tblBorders>'
)
tblPr.append(borders)
def make_callout_box(doc, text, title="IMPORTANT"):
"""호출 박스 (Alert Box) 생성"""
tbl = doc.add_table(rows=1, cols=1)
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
tbl.autofit = False
cell = tbl.cell(0, 0)
cell.width = Inches(6.5)
set_cell_background(cell, "F0F4F8")
set_cell_margins(cell, top=140, bottom=140, left=200, right=200)
# 왼쪽 두꺼운 테두리만 설정
tcPr = cell._tc.get_or_add_tcPr()
borders = parse_xml(
f'<w:tcBorders {nsdecls("w")}>'
f'<w:left w:val="single" w:sz="24" w:space="0" w:color="1F4E79"/>'
f'<w:top w:val="none"/>'
f'<w:right w:val="none"/>'
f'<w:bottom w:val="none"/>'
f'</w:tcBorders>'
)
tcPr.append(borders)
p = cell.paragraphs[0]
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(2)
run_t = p.add_run(f"[{title}] ")
run_t.bold = True
run_t.font.name = "맑은 고딕"
run_t.font.size = Pt(9.5)
run_t.font.color.rgb = RGBColor(31, 78, 121)
run_b = p.add_run(text)
run_b.font.name = "맑은 고딕"
run_b.font.size = Pt(9.5)
run_b.font.color.rgb = RGBColor(50, 50, 50)
doc.add_paragraph().paragraph_format.space_after = Pt(4)
def format_cell(cell, text, bold=False, color=RGBColor(50, 50, 50), size=Pt(9), align=WD_ALIGN_PARAGRAPH.LEFT, font_name="맑은 고딕"):
p = cell.paragraphs[0]
p.alignment = align
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
p.paragraph_format.line_spacing = 1.15
run = p.add_run(text)
run.bold = bold
run.font.name = font_name
run.font.size = size
run.font.color.rgb = color
def create_styled_table(doc, headers, data, col_widths=None, alignments=None):
"""스타일이 적용된 데이터 테이블 생성"""
table = doc.add_table(rows=len(data) + 1, cols=len(headers))
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = False
set_table_borders(table, color="D9D9D9", sz="4")
# 헤더 행
hdr_row = table.rows[0]
# 헤더 반복 (Repeat header row on every page)
trPr = hdr_row._tr.get_or_add_trPr()
trPr.append(parse_xml(f'<w:tblHeader {nsdecls("w")}/>'))
for i, title in enumerate(headers):
cell = hdr_row.cells[i]
set_cell_background(cell, "1F4E79")
set_cell_margins(cell, top=140, bottom=140, left=160, right=160)
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
format_cell(cell, title, bold=True, color=RGBColor(255, 255, 255), size=Pt(9.5), align=WD_ALIGN_PARAGRAPH.CENTER)
if col_widths and i < len(col_widths):
cell.width = col_widths[i]
# 데이터 행
for r_idx, row_data in enumerate(data):
row = table.rows[r_idx + 1]
bg_color = "F9FAFC" if (r_idx % 2 == 1) else "FFFFFF"
for c_idx, cell_value in enumerate(row_data):
cell = row.cells[c_idx]
set_cell_background(cell, bg_color)
set_cell_margins(cell, top=100, bottom=100, left=140, right=140)
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
align = WD_ALIGN_PARAGRAPH.LEFT
if alignments and c_idx < len(alignments):
align = alignments[c_idx]
# 굵은 글씨 여부 (첫 열 또는 코드 열 등)
is_bold = (c_idx == 0 and len(headers) > 3)
format_cell(cell, cell_value, bold=is_bold, color=RGBColor(50, 50, 50), size=Pt(9), align=align)
if col_widths and c_idx < len(col_widths):
cell.width = col_widths[c_idx]
# 테이블 아래 여백 문단
p_after = doc.add_paragraph()
p_after.paragraph_format.space_before = Pt(0)
p_after.paragraph_format.space_after = Pt(6)
def build_protocol_document():
doc = Document()
# 페이지 여백 설정 (A4: 210mm x 297mm, 여백: 20mm)
sections = doc.sections
for section in sections:
section.top_margin = Inches(0.8)
section.bottom_margin = Inches(0.8)
section.left_margin = Inches(0.8)
section.right_margin = Inches(0.8)
# 헤더 & 푸터
header = section.header
hp = header.paragraphs[0]
hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
hrun = hp.add_run("KT mBMS CAN 통신 프로토콜 규격서 (v1.0)")
hrun.font.name = "맑은 고딕"
hrun.font.size = Pt(8.5)
hrun.font.color.rgb = RGBColor(140, 140, 140)
footer = section.footer
fp = footer.paragraphs[0]
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
frun = fp.add_run("AMOTECH Energy Solution Team | Confidential")
frun.font.name = "맑은 고딕"
frun.font.size = Pt(8.5)
frun.font.color.rgb = RGBColor(140, 140, 140)
# 1. 문서 제목
title_p = doc.add_paragraph()
title_p.paragraph_format.space_before = Pt(4)
title_p.paragraph_format.space_after = Pt(4)
title_run = title_p.add_run("KT mBMS CAN 통신 프로토콜 규격서")
title_run.bold = True
title_run.font.name = "맑은 고딕"
title_run.font.size = Pt(21)
title_run.font.color.rgb = RGBColor(31, 78, 121)
sub_p = doc.add_paragraph()
sub_p.paragraph_format.space_before = Pt(0)
sub_p.paragraph_format.space_after = Pt(14)
sub_run = sub_p.add_run("3종 하드웨어 공통 통합 규격 (V1 / V2 / V3 펌웨어 및 관제 GUI 연동)")
sub_run.font.name = "맑은 고딕"
sub_run.font.size = Pt(12)
sub_run.font.color.rgb = RGBColor(80, 80, 80)
# 메타 정보 박스
meta_table = doc.add_table(rows=4, cols=2)
meta_table.alignment = WD_TABLE_ALIGNMENT.CENTER
set_table_borders(meta_table, color="D0D7DE", sz="4")
meta_data = [
("문서 버전 / 작성일자", "V1.0.0 / 2026년 10월 22일"),
("적용 대상 펌웨어", "• V1 (1st HW): mBMS_APP_V2403 (STM32F407 + BQ76PL455, 외부 릴레이 제어)\n• V2 (2nd HW): APP_R3221 (HC32F460 + BQ76940, 외부 릴레이 제어)\n• V3 (3rd HW): APP_R3323 (HC32F460 + ADBMS1818, 외부 릴레이 제어)"),
("참조 프로그램", "LFP_Manager_GL (V3.3.0.16) - KT 표준 모니터링/진단 GUI 프로그램"),
("주요 통신 사양", "CAN 2.0B Extended Frame (29-bit Identifier, 250 kbps, Base ID: 200)")
]
for r_i, (k, v) in enumerate(meta_data):
row = meta_table.rows[r_i]
c0 = row.cells[0]
c1 = row.cells[1]
c0.width = Inches(2.0)
c1.width = Inches(4.5)
set_cell_background(c0, "F2F4F8")
set_cell_background(c1, "FFFFFF")
set_cell_margins(c0, top=80, bottom=80, left=120, right=120)
set_cell_margins(c1, top=80, bottom=80, left=120, right=120)
format_cell(c0, k, bold=True, color=RGBColor(31, 78, 121), size=Pt(9))
format_cell(c1, v, bold=False, color=RGBColor(40, 40, 40), size=Pt(9))
doc.add_paragraph().paragraph_format.space_after = Pt(10)
# 섹션 1
h1 = doc.add_heading("1. 개요 및 물리 계층 (Physical Layer)", level=1)
h1.paragraph_format.space_before = Pt(14)
h1.paragraph_format.space_after = Pt(6)
p = doc.add_paragraph("본 규격서는 KT mBMS 하드웨어 3종(V1, V2, V3)과 상위 관제 시스템(호스트 GUI, 랙 제어기 등) 간의 표준 CAN(Controller Area Network) 통신 프로토콜을 정의합니다. 모든 하드웨어(V1, V2, V3)는 보드 내부의 N-MOSFET을 제어하여 외부 대용량 릴레이(충전/방전/프리차지 릴레이)를 구동하는 동일한 전력 제어 구조를 공유하며, 보드 외적인 시스템 구성 및 배선은 완전히 동일합니다. 보드 내부의 MCU 및 AFE IC 등 모니터링 엔진의 차이에 관계없이 동일한 통신 프레임 규격 및 데이터 맵을 100% 공유합니다.")
p.paragraph_format.space_after = Pt(8)
create_styled_table(
doc,
headers=["항목 (Item)", "사양 (Specification)", "비고"],
data=[
["통신 규격", "CAN 2.0B Extended Frame (29-bit Identifier)", "SAE J1939 PDU1 구조 준용"],
["통신 속도 (Baud Rate)", "250 kbps (기본 표준 속도)", "125k, 250k, 500k, 1Mbps 가변 지원 가능"],
["종단 저항 (Termination)", "120 Ω (양 끝단 장착)", "차동 신호 반사 방지"],
["데이터 길이 (DLC)", "8 Bytes 고정", "미사용 바이트는 0xFF 또는 0x00 패딩"],
["기본 노드 주소 (Base Addr)", "200 (0xC8)", "모듈 ID(1~16)에 따라 201 ~ 216 자동 할당"],
["브로드캐스트 주소", "255 (0xFF)", "전체 BMS 모듈 동시 지시"],
["호스트(Master/GUI) 주소", "1 또는 200", "GUI 송신 시 SA=1, PS=200+ID"]
],
col_widths=[Inches(2.2), Inches(3.2), Inches(1.5)],
alignments=[WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
# 섹션 2
h1 = doc.add_heading("2. CAN 29-bit Extended Identifier 구조", level=1)
h1.paragraph_format.space_before = Pt(14)
h1.paragraph_format.space_after = Pt(6)
p = doc.add_paragraph("CAN 메시지 식별자는 SAE J1939 PDU1 (Point-to-Point 및 Broadcast) 방식을 기반으로 다음과 같이 29비트가 배치됩니다.")
p.paragraph_format.space_after = Pt(6)
# 비트 구조 테이블
create_styled_table(
doc,
headers=["Priority / Index", "Reserved (R)", "Data Page (DP)", "PDU Format (PF)", "PDU Specific (PS)", "Source Addr (SA)"],
data=[
["Bit 28 ~ 26 (3b)", "Bit 25 (1b)", "Bit 24 (1b)", "Bit 23 ~ 16 (8b)", "Bit 15 ~ 8 (8b)", "Bit 7 ~ 0 (8b)"],
["서비스 우선순위", "0 고정", "0 고정", "명령어 코드(Command)", "수신자 주소(DA)", "송신자 주소(SA)"]
],
col_widths=[Inches(1.2), Inches(0.9), Inches(1.0), Inches(1.3), Inches(1.2), Inches(1.1)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER] * 6
)
create_styled_table(
doc,
headers=["필드명", "비트 범위", "크기", "설명 및 규칙"],
data=[
["Index (Priority)", "Bit 28 ~ 26", "3 bits", "• 0x6 (6): 일반 모니터링 / 제어 / 파라미터 셋업\n• 0x1 (1): 제조정보 / 인벤토리 데이터 (Inventory Data)\n• 0x4 (4): 펌웨어 다운로드 및 원격 업데이트 (F/W Update)"],
["R (Reserved)", "Bit 25", "1 bit", "항상 0 고정"],
["DP (Data Page)", "Bit 24", "1 bit", "기본 0 고정 (확장 커맨드 시 1)"],
["PF (PDU Format)", "Bit 23 ~ 16", "8 bits", "명령어 코드 (Command / Function Code)\n• 호스트 요청 코드 (Request Code: 1 ~ 25)\n• BMS 응답 코드 (Response Code: 11, 12, 13, 191~194, 31, 32 등)"],
["PS (PDU Specific)", "Bit 15 ~ 8", "8 bits", "수신 대상 주소 (Destination Address / Target)\n• 호스트 → BMS: 200 + Target_ID (1번 모듈 = 201 / 전체 = 0xFF)\n• BMS → 호스트: 호스트 주소 (200 또는 1)"],
["SA (Source Addr)", "Bit 7 ~ 0", "8 bits", "송신자 주소 (Source Address)\n• 호스트 → BMS: 1 (호스트 식별 주소)\n• BMS → 호스트: 200 + My_ID (자신의 노드 주소, 예: 201)"]
],
col_widths=[Inches(1.5), Inches(1.1), Inches(0.8), Inches(3.4)],
alignments=[WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT]
)
# 섹션 3
h1 = doc.add_heading("3. 일반 모니터링 및 제어 프로토콜 (Index = 6)", level=1)
h1.paragraph_format.space_before = Pt(14)
h1.paragraph_format.space_after = Pt(6)
p = doc.add_paragraph("호스트(GUI)가 특정 슬레이브 ID를 대상으로 데이터를 요청(Request)하면, 대상 BMS가 즉시 해당 데이터를 포함한 응답(Response) 패킷을 1개 또는 복수 개 연속 전송합니다. 파라미터 설정 시에는 요청 패킷 내에 유효 설정 값을 담아 전송하며, 단순 조회 시에는 데이터 바이트를 0xFF로 채워 송신합니다.")
p.paragraph_format.space_after = Pt(8)
cmd_data = [
["시스템 종합 상태", "1", "11, 12, 13, 14", "팩전압, 전류, SOC, 운용상태, 경보/보호 비트, 펌웨어 버전 (연속 4패킷)"],
["셀 개별 전압", "19", "191 ~ 194", "Cell #1 ~ #16 개별 전압 측정값 (1mV 단위, 4개 패킷 분할)"],
["온도 계측값", "3", "31, 32", "Cell 온도 6개 + BMS 기판 / FET 주변 온도 2개 (0.1℃ 단위)"],
["SOC / SOH", "4", "41", "정밀 상대 SOC(0.1%), SOH(%), 충방전 사이클 수"],
["셀/온도 편차 파라미터", "2", "21", "셀 전압 편차(mV) 및 온도 편차(0.1℃), 각각의 보호 시간(분 단위) 통합 설정 및 응답"],
["셀 전압 파라미터", "5", "51", "CUV / COV Warning 및 Trip 임계치 (mV 단위)"],
["온도 보호 파라미터", "6", "61", "충/방전 저온 및 고온 Warning / Trip 임계치 (+80 Offset, ℃)"],
["팩 전압 파라미터", "7", "71", "SUV / SOV Warning 및 Trip 임계치 (0.01V 단위)"],
["배터리 구성 사양", "8", "81", "셀 수량(15/16), 온도센서 수량, 공칭 용량(0.1Ah)"],
["셀 밸런싱 파라미터", "10", "101", "자동 밸런싱 개시 전압, 편차 윈도우, 주기"],
["셀 밸런싱 상태", "11", "111", "실시간 밸런싱 동작 여부 및 셀별 동작 비트맵"],
["수동 밸런싱 제어", "12", "-", "특정 셀 강제 밸런싱 수동 ON / OFF 지시"],
["SOC 강제 설정", "13", "-", "현장 SOC 값 수동 보정 (0 ~ 100%)"],
["Low SOC 파라미터", "14", "141", "저용량 경보 및 복구 SOC 임계치 (%)"],
["옵션 파라미터", "15", "151", "충전 옵션 및 시스템 플래그"],
["기본값 복원/셧다운", "16", "-", "공장 출하 파라미터 복원(Default) 또는 강제 차단 제어"],
["전류 캘리브레이션", "17", "171", "충/방전 전류 센서 게인(Gain) 및 션트 레인지 보정"],
["팩전압 캘리브레이션", "18", "181", "팩 전압 센서 게인 보정"],
["하트비트 / 샘플링", "20", "201", "BMS 누적 카운터 및 1회 AFE 샘플링 소요 시간(ms)"],
["RTC 일시 동기화", "21", "211", "현재 날짜/시간(년,월,일,시,분,초) 및 최종 리셋 원인"],
["과전류 보호 파라미터", "22", "221", "충/방전 과전류(OCC/OCD) Warning 및 Trip (+30000, 0.1A)"],
["전압 복구 파라미터", "23", "231", "CUV, COV, SUV, SOV 복구 전압 임계치"],
["온도/전류 복구 파라미터", "24", "241", "저온/고온 및 과전류 복구 임계치"],
["원격 소프트 리셋", "25", "-", "BMS 시스템 원격 재부팅 수행"]
]
create_styled_table(
doc,
headers=["기능 (Function)", "요청 PF", "응답 PF", "설명 및 페이로드 요약"],
data=cmd_data,
col_widths=[Inches(1.8), Inches(0.8), Inches(1.1), Inches(3.1)],
alignments=[WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT]
)
# 섹션 4
h1 = doc.add_heading("4. 메시지별 상세 데이터 페이로드 규격", level=1)
h1.paragraph_format.space_before = Pt(14)
h1.paragraph_format.space_after = Pt(6)
# 4.1 시스템 종합 상태
h2 = doc.add_heading("4.1 시스템 종합 상태 (PF: 1 → 11, 12, 13, 14)", level=2)
h2.paragraph_format.space_before = Pt(10)
h2.paragraph_format.space_after = Pt(4)
p = doc.add_paragraph("• 호스트 요청 (PF: 1): Data[0~5]=0xFF, Data[6]=DCP Flag(0x01: 강제차단 연동), Data[7]=0xFF(상태요청) / 0 or 1(자동 밸런싱 On/Off)\n• BMS 응답: PF 11, 12, 13, 14 총 4개의 연속 패킷을 5ms 간격으로 송신합니다.")
p.paragraph_format.space_after = Pt(6)
doc.add_heading("▶ Status #1 (PF: 11) - 기본 계측 및 알람 1", level=3)
create_styled_table(
doc,
headers=["Byte", "항목", "타입", "단위/스케일", "상세 설명"],
data=[
["0 ~ 1", "팩 전압 (Battery Voltage)", "uint16", "0.01 V", "예: 5120 = 51.20 V"],
["2", "SOC (State of Charge)", "uint8", "1 %", "현재 배터리 잔여 용량 (0 ~ 100%)"],
["3", "Auto Cell Balancing", "uint8", "0 or 1", "0: 비활성, 1: 자동 밸런싱 동작 중"],
["4", "Warning 상태 비트 1", "uint8", "Bitfield", "[Bit 0] COV Warning [Bit 1] CUV Warning\n[Bit 2] High Temp W [Bit 3] Low Temp W\n[Bit 4] SOV Warning [Bit 5] SUV Warning\n[Bit 6] Cell Voltage Diff Warning"],
["5", "Protection 상태 비트 1", "uint8", "Bitfield", "[Bit 0] COV Fault [Bit 1] CUV Fault\n[Bit 2] High Temp F [Bit 3] Low Temp F\n[Bit 4] SOV Fault [Bit 5] SUV Fault\n[Bit 6] Cell Voltage Diff Fault (24h 차단)\n[Bit 7] AFE Fail / 센서 단선 결함"],
["6 ~ 7", "전류 (Current)", "int16", "0.1 A", "Offset: +30000\n실제 전류 = (수신값 - 30000) × 0.1 A\n(+값: 충전 전류, -값: 방전 전류)"]
],
col_widths=[Inches(0.8), Inches(2.0), Inches(0.8), Inches(1.1), Inches(2.1)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
doc.add_heading("▶ Status #2 (PF: 12) - 알람 2, 동작 상태 및 버전", level=3)
create_styled_table(
doc,
headers=["Byte", "항목명", "타입", "상세 내용"],
data=[
["0", "경고/보호 비트필드 2", "uint8", "[Bit 0] 모듈 내 온도 편차 차단 (3.0℃ 24시간 지속 차단 래치)\n[Bit 1] 충전 과전류 3회 완전 차단 (60초 내 3회 영구 락아웃)\n[Bit 2] Low SOC 경고\n[Bit 4] 충전 고온 경고 [Bit 5] 충전 저온 경고\n[Bit 6] 방전 고온 경고 [Bit 7] 방전 저온 경고"],
["1", "보호 비트필드 2", "uint8", "[Bit 0] 충전 과전류 경고 [Bit 1] 일반 충전 과전류 차단 (자동 복구 가능)\n[Bit 2] 방전 과전류 경고 [Bit 3] 방전 과전류 차단\n[Bit 4] 충전 고온 차단 [Bit 5] 충전 저온 차단\n[Bit 6] 방전 고온 차단 [Bit 7] 방전 저온 차단"],
["2", "동작 상태 (op_status)", "uint8", "0: 대기 (Standby) 1: 충전 (Charging)\n2: 방전 (Discharging) 3: 플로팅 (Floating)\n4: 승온 (Warming Up)"],
["3 ~ 5", "고유 식별자", "-", "Byte 3: 고유 서명 0x57 ('W')\nByte 4: 하드웨어 구분자 (0x11:V1, 0x22:V2, 0x33:V3)\nByte 5: 빌드 리비전 0x02 (R402 빌드)"],
["6 ~ 7", "펌웨어 버전", "uint16", "4-nibble BCD 포맷 (예: 0x4001 -> V4.0.0.1)"]
],
col_widths=[Inches(0.8), Inches(2.2), Inches(0.8), Inches(3.0)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT]
)
doc.add_heading("▶ Status #3 (PF: 13) & Status #4 (PF: 14)", level=3)
create_styled_table(
doc,
headers=["응답 PF", "Byte", "항목", "상세 설명"],
data=[
["PF 13", "0 ~ 1", "battery_status.value", "내부 상태 레지스터 (Bit 0: INIT, Bit 1: CHG_C, Bit 2: DSG_C, Bit 3: ZVC_C, Bit 4: CB, Bit 5: ACB, Bit 6: TCA, Bit 7: TDA 등)"],
["PF 13", "2 ~ 3", "protect.value", "16비트 전체 보호 플래그 레지스터"],
["PF 13", "4 ~ 7", "Reserved", "0x00"],
["PF 14", "0 ~ 1", "COV Protect Cell Mask", "과전압 보호 발생 셀 비트마스크 (Bit 0~14: Cell 1~15)"],
["PF 14", "2 ~ 3", "COV Warning Cell Mask", "과전압 경보 발생 셀 비트마스크"],
["PF 14", "4 ~ 5", "CUV Protect Cell Mask", "저전압 보호 발생 셀 비트마스크"],
["PF 14", "6 ~ 7", "CUV Warning Cell Mask", "저전압 경보 발생 셀 비트마스크"]
],
col_widths=[Inches(0.9), Inches(0.8), Inches(2.1), Inches(3.0)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
# 4.2 셀 개별 전압
h2 = doc.add_heading("4.2 셀 개별 전압 계측 (PF: 19 → 191 ~ 194)", level=2)
h2.paragraph_format.space_before = Pt(10)
h2.paragraph_format.space_after = Pt(4)
p = doc.add_paragraph("호스트가 PF=19를 요청하면 BMS는 4개의 연속 패킷을 5ms 간격으로 전송합니다. 전압 단위는 1mV입니다.")
p.paragraph_format.space_after = Pt(6)
create_styled_table(
doc,
headers=["응답 PF", "Byte 0 ~ 1", "Byte 2 ~ 3", "Byte 4 ~ 5", "Byte 6 ~ 7", "단위"],
data=[
["PF 191", "Cell #1 전압", "Cell #2 전압", "Cell #3 전압", "Cell #4 전압", "1 mV"],
["PF 192", "Cell #5 전압", "Cell #6 전압", "Cell #7 전압", "Cell #8 전압", "1 mV"],
["PF 193", "Cell #9 전압", "Cell #10 전압", "Cell #11 전압", "Cell #12 전압", "1 mV"],
["PF 194", "Cell #13 전압", "Cell #14 전압", "Cell #15 전압", "Cell #16 전압 (15S는 0)", "1 mV"]
],
col_widths=[Inches(1.1), Inches(1.3), Inches(1.3), Inches(1.3), Inches(1.3), Inches(0.5)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER] * 6
)
# 4.3 온도 계측
h2 = doc.add_heading("4.3 온도 계측 모니터링 (PF: 3 → 31, 32)", level=2)
h2.paragraph_format.space_before = Pt(10)
h2.paragraph_format.space_after = Pt(4)
p = doc.add_paragraph("온도 데이터는 Offset: +800 (0.1℃ 단위) 형식으로 전송됩니다.\n• 실제 온도(℃) = (수신값 - 800) / 10 (예: 25.0℃ = 1050 전송, -10.0℃ = 700 전송)")
p.paragraph_format.space_after = Pt(6)
create_styled_table(
doc,
headers=["응답 PF", "Byte 0 ~ 1", "Byte 2 ~ 3", "Byte 4 ~ 5", "Byte 6 ~ 7", "비고"],
data=[
["PF 31", "Temp #1 (Cell 1)", "Temp #2 (Cell 2)", "Temp #3 (Cell 3)", "Temp #4 (Cell 4)", "배터리 셀 표면 서미스터"],
["PF 32", "Temp #5 (Cell 5)", "Temp #6 (Cell 6)", "Ext #1 (BMS기판)", "Ext #2 (FET/주변)", "BMS 내부 환경 온도"]
],
col_widths=[Inches(1.0), Inches(1.3), Inches(1.3), Inches(1.3), Inches(1.3), Inches(1.6)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
# 4.4 셀 전압 및 온도 편차 파라미터 통합 패킷 (KT 핵심 규격)
h2 = doc.add_heading("4.4 셀 및 온도 편차 파라미터 (PF: 2 ↔ 21) [KT 핵심 요구사항]", level=2)
h2.paragraph_format.space_before = Pt(10)
h2.paragraph_format.space_after = Pt(4)
p = doc.add_paragraph("• 조회: Data[0~7]=0xFF 전송 시 BMS가 현재 설정값으로 PF=21 응답\n• 설정: 아래 8바이트 값을 담아 PF=2 전송 시 파라미터 갱신 (RAM 운용, 리셋 시 초기값 복원)\n• 독립적 갱신: 각 임계값(Threshold)이 0 또는 0xFFFF인 경우 해당 파라미터(임계치 및 시간)는 변경하지 않고 기존값을 유지합니다.")
p.paragraph_format.space_after = Pt(6)
create_styled_table(
doc,
headers=["Byte", "파라미터 항목", "타입", "기본값 (KT 규격)", "상세 설명"],
data=[
["0 ~ 1", "Cell Voltage Diff Threshold", "uint16", "200 mV", "셀 전압 편차 차단 기준치 (KT 규격: 200mV 이상 지속 시 차단, 0/0xFFFF 시 유지)"],
["2 ~ 3", "Temperature Diff Threshold", "uint16", "30 (3.0℃)", "온도 편차 차단 기준치 (0.1℃ 단위, 3.0℃ 이상 지속 시 차단, 0/0xFFFF 시 유지)"],
["4 ~ 5", "Cell Voltage Diff Hold Time", "uint16", "1440 min (24h)", "셀 전압 편차 지속 보호 시간 (분/min 단위, 0/0xFFFF 시 유지)"],
["6 ~ 7", "Temperature Diff Hold Time", "uint16", "1440 min (24h)", "온도 편차 지속 보호 시간 (분/min 단위, 0/0xFFFF 시 유지)"]
],
col_widths=[Inches(0.8), Inches(2.2), Inches(0.8), Inches(1.3), Inches(2.4)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT]
)
make_callout_box(
doc,
"24시간 지속 차단 로직: 셀 전압 편차가 200mV 이상으로 24시간(1,440분 = 86,400초) 동안 지속 유지될 경우 protect.bit.cell_voltage_diff 플래그가 확정되어 영구 차단됩니다. 온도 편차 역시 3.0℃ 이상으로 24시간 지속 유지 시 protect.bit.temp_diff 플래그가 확정되어 차단됩니다. CAN 프레임 Byte 4~5와 Byte 6~7을 통해 전압편차 시간과 온도편차 시간을 분(min) 단위로 각각 독립 설정 및 모니터링할 수 있습니다.",
title="KT 핵심 요구사항 (셀 전압 200mV 및 온도 3.0℃ 24시간 차단 통합 제어)"
)
# 4.5 기타 보호 파라미터
h2 = doc.add_heading("4.5 보호 및 복구 파라미터 셋업 규격", level=2)
h2.paragraph_format.space_before = Pt(10)
h2.paragraph_format.space_after = Pt(4)
create_styled_table(
doc,
headers=["기능", "요청/응답 PF", "Byte 0 ~ 1", "Byte 2 ~ 3", "Byte 4 ~ 5", "Byte 6 ~ 7"],
data=[
["셀 전압 보호", "5 / 51", "CUV Warning (mV)", "CUV Trip (mV)", "COV Warning (mV)", "COV Trip (mV)"],
["전압 복구", "23 / 231", "CUV Recovery (mV)", "COV Recovery (mV)", "SUV Recovery (0.01V)", "SOV Recovery (0.01V)"],
["팩 전압 보호", "7 / 71", "SUV Warning (0.01V)", "SUV Trip (0.01V)", "SOV Warning (0.01V)", "SOV Trip (0.01V)"],
["온도 보호", "6 / 61", "충전저온 W/T (+80)", "충주고온 W/T (+80)", "방전저온 W/T (+80)", "방전고온 W/T (+80)"],
["온도/전류 복구", "24 / 241", "충전저온/고온 복구", "방전저온/고온 복구", "충전과전류 복구(+3만)", "방전과전류 복구(+3만)"],
["과전류 보호", "22 / 221", "충전과전류 W (+3만)", "충전과전류 T (+3만)", "방전과전류 W (+3만)", "방전과전류 T (+3만)"]
],
col_widths=[Inches(1.2), Inches(1.0), Inches(1.3), Inches(1.3), Inches(1.3), Inches(1.3)],
alignments=[WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
make_callout_box(
doc,
"반복 OCC 3회 영구 래치 차단 (Lockout): 충전 과전류(OCC)가 1분 이내 3회 반복 발생하면 OCC_3Times_Fail = true 로 래치되어, 충전 전류가 정상으로 내려가더라도 자동 복구되지 않고 충전 스위치가 영구 차단 유지됩니다.",
title="KT 핵심 요구사항 (OCC 3회 영구 래치)"
)
# 4.6 하트비트 및 RTC
h2 = doc.add_heading("4.6 하트비트 및 시스템 일시(RTC) 동기화", level=2)
h2.paragraph_format.space_before = Pt(10)
h2.paragraph_format.space_after = Pt(4)
create_styled_table(
doc,
headers=["기능", "PF", "바이트 구성", "설명"],
data=[
["하트비트 & 카운트다운", "20 -> 201", "Byte 0~3: Heartbeat 카운터 (ms Tick, Big-Endian)\nByte 4~5: 예약 (0x0000)\nByte 6~7: 편차 차단 잔여 시간 (GetCvDiff(), 초)", "셀 전압(200mV) 및 모듈온도(3.0℃) 편차 차단까지 남은 시간. 0초 도달 시 충전 차단 후 MCU 리셋 전까지 영구 래치."],
["RTC 일시 동기화", "21 -> 211", "Byte 0: 연 (Year - 1970, 예: 2026년 = 56)\nByte 1: 월 (1~12) Byte 2: 일 (1~31)\nByte 3: 시 (0~23) Byte 4: 분 (0~59) Byte 5: 초 (0~59)\nByte 6~7: [응답] 최근 리셋 원인 (Reboot Reason)", "상위 제어 시스템 시간과 BMS 내부 RTC 클럭 동기화 및 리셋 원인 진단"]
],
col_widths=[Inches(1.3), Inches(0.9), Inches(2.9), Inches(2.3)],
alignments=[WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
# 섹션 5
h1 = doc.add_heading("5. 인벤토리 및 식별자 관리 프로토콜 (Index = 1)", level=1)
h1.paragraph_format.space_before = Pt(14)
h1.paragraph_format.space_after = Pt(6)
p = doc.add_paragraph("배터리 팩 모듈의 생산 이력 및 부품 추적성을 위해 제조일자, BMS 일련번호, 모듈 일련번호를 전용 채널(Index = 1)로 관리합니다.")
p.paragraph_format.space_after = Pt(6)
create_styled_table(
doc,
headers=["요청 PF", "응답 PF", "식별자 항목", "데이터 포맷", "크기 및 내용"],
data=[
["1", "11", "BMS 제조일자 (Manufacture Date)", "4 Bytes (uint32)", "YYYYMMDD 숫자형 정수"],
["2", "21", "BMS Serial Number (상위 8자리)", "8 Bytes ASCII", "BMS 보드 고유 일련번호 [0~7]"],
["3", "31", "BMS Serial Number (하위 8자리)", "8 Bytes ASCII", "BMS 보드 고유 일련번호 [8~15] (총 16자리)"],
["4", "41", "Module Serial Number #1", "8 Bytes ASCII", "배터리 팩 모듈 일련번호 [0~7]"],
["5", "51", "Module Serial Number #2", "8 Bytes ASCII", "배터리 팩 모듈 일련번호 [8~15]"],
["6", "61", "Module Serial Number #3", "8 Bytes ASCII", "배터리 팩 모듈 일련번호 [16~23]"],
["7", "71", "Module Serial Number #4", "8 Bytes ASCII", "배터리 팩 모듈 일련번호 [24~31] (총 32자리)"]
],
col_widths=[Inches(0.8), Inches(0.8), Inches(2.2), Inches(1.5), Inches(2.1)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
# 섹션 6
h1 = doc.add_heading("6. 펌웨어 무선/원격 다운로드 프로토콜 (Index = 4)", level=1)
h1.paragraph_format.space_before = Pt(14)
h1.paragraph_format.space_after = Pt(6)
p = doc.add_paragraph("통합 관제 프로그램(LFP_Manager_GL)에서 현장 BMS 모듈의 펌웨어를 원격 업데이트할 때 사용하는 프로토콜(Index = 4)입니다.")
p.paragraph_format.space_after = Pt(6)
create_styled_table(
doc,
headers=["기능 코드 (FC)", "명칭 (Command)", "설명 및 동작"],
data=[
["0", "DL_START", "F/W 업데이트 모드 진입 지시 (BMS 수락 확인)"],
["1", "DL_SECTOR_ERASE", "타깃 MCU Flash App 섹터 삭제 지시"],
["2", "DL_SET_ADDRESS", "기록할 Flash 시작 번지 주소 설정"],
["3", "DL_READ_DATA", "Flash 기록 데이터 사후 검증용 읽기"],
["4", "DL_WRITE_DATA", "바이너리 블록 데이터 전송 및 플래시 Write"],
["5", "DL_WRITE_DATA_CSUM", "전송 블록별 Checksum 무결성 검증"],
["6", "DL_IMAGE_CSUM", "전체 펌웨어 바이너리 최종 Checksum 확인"],
["7", "DL_RESTART", "다운로드 완료 후 부트로더 재부팅 및 신규 펌웨어 기동"]
],
col_widths=[Inches(1.2), Inches(2.0), Inches(4.2)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
# 섹션 7
h1 = doc.add_heading("7. GUI 통신 시퀀스 및 폴링 가이드 (LFP_Manager_GL)", level=1)
h1.paragraph_format.space_before = Pt(14)
h1.paragraph_format.space_after = Pt(6)
p = doc.add_paragraph("LFP_Manager_GL 관제 프로그램은 CAN 버스 트래픽 과부하를 방지하면서도 실시간 모니터링을 달성하기 위해 다음과 같은 2계층 폴링 주기를 적용합니다.")
p.paragraph_format.space_after = Pt(6)
create_styled_table(
doc,
headers=["폴링 계층", "주기", "요청 커맨드 (PF)", "수집 정보"],
data=[
["1계층: 고속 실시간 상태", "1.0 초", "PF: 1 (Status Req)", "팩전압, SOC, 전류, 알람/보호 비트, 충방전 운용상태"],
["2계층: 정밀 계측 수집", "2.0 ~ 3.0 초", "PF: 19 (Cell V)\nPF: 3 (Temp)\nPF: 4 (SOC/SOH)", "16개 셀 전압, 8개 채널 온도, SOH, 충방전 사이클 수"],
["파라미터 화면 진입 시", "1회성 일괄 조회", "PF: 5, 6, 7, 14, 22, 23, 24, 2", "전압/온도/전류 보호 임계치 및 복구치 일괄 동기화 (50ms 간격)"]
],
col_widths=[Inches(1.8), Inches(1.1), Inches(2.0), Inches(2.5)],
alignments=[WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT]
)
make_callout_box(
doc,
"통신 지연 권장값: BMS가 복수 개의 응답 패킷을 전송할 때 패킷 간 최소 5ms 딜레이(CAN_TX_CONT_DELAY = 5ms)를 두어 CAN 버퍼 오버런을 방지합니다. 호스트 응답 대기 타임아웃은 500ms를 권장합니다.",
title="통신 타이밍 권장사항"
)
# 섹션 8
h1 = doc.add_heading("8. 하드웨어 3종별 펌웨어 구현 및 검증 결과", level=1)
h1.paragraph_format.space_before = Pt(14)
h1.paragraph_format.space_after = Pt(6)
p = doc.add_paragraph("본 CAN 프로토콜 규격은 하드웨어 3종에 각각 최적화된 펌웨어에 구현되어 있으며, Keil MDK ARMCC 컴파일러를 통해 빌드 검증 및 GUI 연동 테스트를 완료하였습니다.")
p.paragraph_format.space_after = Pt(6)
create_styled_table(
doc,
headers=["구분", "하드웨어 스펙", "관련 구현 소스 파일", "빌드 검증 결과"],
data=[
["V1 펌웨어\n(mBMS_APP_V2403)", "STM32F407VGT6\nTI BQ76PL455 (UART)\n외부 릴레이 제어 (N-MOSFET 구동)", "APP/can_comm.c\nAPP/can_data_process.c\nAPP/can_update_process.c", "98개 C 파일 전체 빌드 성공\n(0 Error, ARMCC v5.06)"],
["V2 펌웨어\n(APP_R3221)", "HC32F460PETB\nTI BQ76940 (I2C)\n외부 릴레이 제어 (N-MOSFET 구동)", "App/Source/can_comm.c\nApp/Source/can_data_process.c\nApp/Source/can_inv_data_process.c", "28개 C 파일 전체 빌드 성공\n(0 Error, ARMCC v5.06)"],
["V3 펌웨어\n(APP_R3323)", "HC32F460PETB\nADI ADBMS1818 (SPI)\n외부 릴레이 제어 (N-MOSFET 구동)", "App/Source/can_comm.c\nApp/Source/can_data_process.c\nApp/Source/can_inv_data_process.c", "27개 C 파일 전체 빌드 성공\n(0 Error, ARMCC v5.06)"]
],
col_widths=[Inches(1.5), Inches(1.8), Inches(2.4), Inches(1.7)],
alignments=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.CENTER]
)
output_path = r"d:\Project\Firmware\KT\mBMS\R4xx\Docs\Protocol\KT_mBMS_CAN_통신프로토콜_규격서.docx"
try:
doc.save(output_path)
print(f"Successfully generated: {output_path}")
except PermissionError:
output_path_alt = r"d:\Project\Firmware\KT\mBMS\R4xx\Docs\Protocol\KT_mBMS_CAN_통신프로토콜_규격서_Rev1.docx"
doc.save(output_path_alt)
print(f"File locked by Word. Successfully generated alternative: {output_path_alt}")
if __name__ == "__main__":
build_protocol_document()