130б. Тепло на плате: тепловое сопротивление, площадь меди и переходные под корпусом
В разделе 55.4 температуру кристалла стабилизатора считали одной строкой:
Tj = Tокр + P × Rth, и для SOT-223 брали 60 °C/Вт. В разделе 204.4 —
то же для микроконтроллера, с θJA из даташитов STM32. Но откуда берётся
это число и почему у одного корпуса бывает и 60, и 150? Тепловое
сопротивление «кристалл — воздух» — свойство не корпуса, а корпуса
вместе с платой: сколько под ним меди, какой толщины, есть ли нижний
слой и чем он соединён с верхним.
Здесь это посчитано. Своя модель платы на компьютере — сетка ячеек по 0,25 мм, медь и стеклотекстолит, переходные отверстия, отдача тепла в воздух — отвечает на практические вопросы: сколько квадратных миллиметров меди нужно стабилизатору на 0,5–1,5 Вт, сколько ставить переходных и когда они бесполезны. Потом та же модель считает узел питания на плате из раздела 131 в четырёх вариантах, собранных скриптом KiCad и проверенных DRC, и сверяется с таблицами θJA из даташитов F103 и F407.
Опоры: даташиты STM32F103x8/B (DocID13587 Rev 17, раздел 6.9, Table 62;
корпуса UFQFPN48 и VFQFPN36 — Tables 52 и 51) и STM32F405/407 (DS8626 Rev 9, раздел 6.7,
Table 98). Идея — заметка TI SNVA419 о тепловом расчёте через
«резисторную» цепочку; её текст и графики в разделе не использованы.
Модель и все листинги — свои: heat130b.py (модель), study130b.py
(опыты), kicad130b.py (плата в KiCad), ref130b.py (независимая
сверка).
Как это называется в документации
Оригинал Перевод thermal resistance junction-ambient, ΘJA тепловое сопротивление кристалл — среда junction-to-case, ΘJC кристалл — корпус (у SOT-223 — кристалл — фланец) junction temperature, TJ температура кристалла power dissipation, PD рассеиваемая мощность natural convection (still air) естественная конвекция, неподвижный воздух exposed pad (exposed die pad) открытая площадка под корпусом thermal via переходное отверстие для отвода тепла copper pour, copper area полигон, площадь меди thermal relief тепловые перемычки между площадкой и полигоном heat spreading растекание тепла plating thickness толщина металлизации отверстия 1s / 2s2p test board тестовая плата: один слой / два сигнальных и два сплошных
130б.1 θJA — свойство платы, а не корпуса
Тепло от кристалла идёт цепочкой: кристалл — фланец или выводы — медь платы — через стеклотекстолит или переходные отверстия на другой слой — в воздух с обеих сторон. Каждое звено — тепловое сопротивление в К/Вт (это то же, что °C/Вт), и звенья последовательно складываются, как резисторы. Перегрев кристалла над воздухом — мощность, умноженная на сумму.
Внутреннее звено — «кристалл — корпус», θJC — задаёт производитель, и
оно мало. Всё остальное — плата. Поэтому θJA из даташита — это цепочка
целиком на плате производителя. Что за плата, даташиты STM32 не
пишут: в обоих сказано только, что условия — JESD51-2, естественная
конвекция в неподвижном воздухе, и формула TJ max = TA max + (PD max × ΘJA).
Видно, что дело в плате, по самим таблицам. У F103 корпуса LQFP48 и UFQFPN48 оба 7 × 7 мм с шагом 0,5 мм, а θJA — 55 и 32 °C/Вт. Разница в одном: у UFQFPN48 снизу открытая площадка 5,6 × 5,6 мм (D2 = E2 = 5,600 типовое), припаянная к плате. Даташит советует подключить её к VSS (примечание 2 к рисунку 44). А у VFQFPN36 площадка меньше — 3,7 мм, — но θJA всего 18 °C/Вт: по одной таблице корпуса не сравнить, если не знать, на каких платах их мерили.
| Корпус | θJA, F103 | θJA, F407 |
|---|---|---|
| LQFP48 7 × 7 мм | 55 °C/Вт | — |
| UFQFPN48 7 × 7 мм, открытая площадка | 32 °C/Вт | — |
| VFQFPN36 6 × 6 мм, открытая площадка | 18 °C/Вт | — |
| LQFP64 10 × 10 мм | 45 °C/Вт | 46 °C/Вт |
| LQFP100 14 × 14 мм | 46 °C/Вт | 43 °C/Вт |
| LQFP144 20 × 20 мм | — | 40 °C/Вт |
Что с этим делать на своей плате, покажет модель: она считает звенья правее фланца — «площадка — воздух» — для любой меди.
130б.2 Модель: сетка ячеек, медь и стеклотекстолит
Плата делится на квадратные ячейки со стороной 0,25 мм. Каждый слой меди — свой лист узлов. Внутри слоя соседние узлы соединены проводимостью меди, если она в ячейке есть, плюс своей доли стеклотекстолита вдоль платы. Слои между собой — проводимостью FR-4 поперёк и стенками переходных отверстий. С верхней и нижней поверхности тепло уходит в воздух. Края платы теплоизолированы. Источник — мощность 1 Вт, поровну на ячейки площадки детали. Тепловое сопротивление «площадка — воздух» — средний перегрев этих ячеек.
Свойства материалов приняты, а не измерены:
| Величина | Принято |
|---|---|
| теплопроводность меди | 390 Вт/(м·К) |
| FR-4 поперёк платы | 0,30 Вт/(м·К) |
| FR-4 вдоль платы | 0,80 Вт/(м·К) |
| отдача в воздух с каждой стороны (конвекция и излучение) | h = 10 Вт/(м²·К) |
| медь на стенке отверстия | 25 мкм |
| толщина платы | 1,6 мм |
Получается система линейных уравнений «проводимости × температуры =
мощности». Матрица симметричная и положительно определённая — её решают
методом сопряжённых градиентов. Саму матрицу не хранят: функция apply
считает потоки срезами массивов numpy. Модель целиком:
# -*- coding: utf-8 -*-
"""Раздел 130б: тепловая модель печатной платы методом конечных разностей.
Плата — сетка квадратных ячеек со стороной d. Каждый слой меди — свой лист узлов; медь есть там,
где маска слоя True. Между соседними узлами слоя тепло течёт по меди и по стеклотекстолиту FR-4,
между слоями — поперёк FR-4 и по стенкам переходных отверстий. С верхней и нижней поверхности
тепло уходит в воздух с коэффициентом h. Источник — мощность P, равномерно на площадке детали.
Нужен только numpy."""
import math
import numpy as np
K_CU = 390.0 # Вт/(м·К), теплопроводность меди (принято)
K_FR4_Z = 0.30 # Вт/(м·К), FR-4 поперёк платы (принято)
K_FR4_XY = 0.80 # Вт/(м·К), FR-4 вдоль платы (принято)
H_AIR = 10.0 # Вт/(м²·К) с каждой стороны: конвекция и излучение в неподвижном воздухе (принято)
T_PLATE = 25e-6 # м, медь на стенке переходного отверстия (принято)
def via_r(drill, plating=T_PLATE, length=1.6e-3):
"""Тепловое сопротивление одного переходного отверстия, К/Вт: медная трубка
с внутренним диаметром drill и стенкой plating, длиной length; середина пустая."""
r_out = drill / 2
r_in = r_out - plating
area = math.pi * (r_out ** 2 - r_in ** 2)
return length / (K_CU * area)
def sheet(t_cu, mask, fr4_share):
"""Проводимость слоя «на квадрат», Вт/К: медь там, где маска, плюс доля FR-4 вдоль платы."""
return K_CU * t_cu * mask + K_FR4_XY * fr4_share
def pair(s1, s2):
"""Проводимость между центрами двух соседних квадратных ячеек: две половинки последовательно."""
return 2 * s1 * s2 / (s1 + s2)
def build(d, masks, t_cu, gaps, vias, h=H_AIR, drill=0.3e-3):
"""Проводимости сетки. masks — список масок слоёв (ny, nx) сверху вниз, t_cu — толщины меди, м,
gaps — толщины FR-4 между соседними слоями, м, vias — число переходных в каждой ячейке (ny, nx),
drill — диаметр отверстия, м.
Возвращает словарь: gx, gy — вдоль слоёв, gz — между слоями, gh — в воздух."""
L = len(masks)
share = [((gaps[l - 1] if l > 0 else 0) + (gaps[l] if l < L - 1 else 0)) / 2 for l in range(L)]
S = np.array([sheet(t_cu[l], masks[l].astype(float), share[l]) for l in range(L)])
gx = pair(S[:, :, :-1], S[:, :, 1:])
gy = pair(S[:, :-1, :], S[:, 1:, :])
via_g = 1.0 / via_r(drill, length=1.0) # проводимость стенки отверстия на метр длины, Вт·м/К
gz = np.array([np.broadcast_to(K_FR4_Z * d * d / g + vias * via_g / g, S[0].shape) for g in gaps])
gh = np.zeros_like(S)
gh[0] = h * d * d
gh[-1] = h * d * d
return {"gx": gx, "gy": gy, "gz": gz, "gh": gh}
def apply(G, T):
"""Поток тепла из каждого узла при температурах T (перегрев над воздухом), Вт: A·T."""
q = G["gh"] * T
f = G["gx"] * (T[:, :, :-1] - T[:, :, 1:])
q[:, :, :-1] += f
q[:, :, 1:] -= f
f = G["gy"] * (T[:, :-1, :] - T[:, 1:, :])
q[:, :-1, :] += f
q[:, 1:, :] -= f
f = G["gz"] * (T[:-1] - T[1:])
q[:-1] += f
q[1:] -= f
return q
def diag(G):
"""Диагональ матрицы A: сумма проводимостей, сходящихся в узле."""
a = G["gh"].copy()
a[:, :, :-1] += G["gx"]
a[:, :, 1:] += G["gx"]
a[:, :-1, :] += G["gy"]
a[:, 1:, :] += G["gy"]
a[:-1] += G["gz"]
a[1:] += G["gz"]
return a
def solve(G, q, tol=1e-9, maxit=50000):
"""Сопряжённые градиенты с диагональным предобусловливанием: A·T = q. Возвращает T и число шагов."""
D = diag(G)
T = np.zeros_like(q)
r = q.copy()
z = r / D
p = z.copy()
rz = np.sum(r * z)
nb = math.sqrt(np.sum(q * q))
for it in range(1, maxit + 1):
Ap = apply(G, p)
a = rz / np.sum(p * Ap)
T += a * p
r -= a * Ap
if math.sqrt(np.sum(r * r)) < tol * nb:
return T, it
z = r / D
rz_new = np.sum(r * z)
p = z + (rz_new / rz) * p
rz = rz_new
raise RuntimeError("CG не сошёлся")
def theta(d, masks, t_cu, gaps, vias, src, P=1.0, h=H_AIR):
"""Тепловое сопротивление площадка–воздух, К/Вт: средний перегрев ячеек источника на ватт.
src — маска ячеек верхнего слоя, куда входит мощность P. Возвращает (θ, баланс, шаги)."""
G = build(d, masks, t_cu, gaps, vias, h)
q = np.zeros((len(masks),) + src.shape)
q[0][src] = P / src.sum()
T, it = solve(G, q)
out = float(np.sum(G["gh"] * T)) # что ушло в воздух — должно быть P
return float(T[0][src].mean()) / P, out / P, it
def rect(ny, nx, d, x0, y0, x1, y1):
"""Маска прямоугольника [x0, x1) × [y0, y1) в метрах по центрам ячеек."""
yc = (np.arange(ny) + 0.5) * d
xc = (np.arange(nx) + 0.5) * d
return ((yc[:, None] >= y0) & (yc[:, None] < y1)) & ((xc[None, :] >= x0) & (xc[None, :] < x1))
Проверено двумя путями (ref130b.py, numpy 2.3.3 и scipy 1.18.1).
Первый — точное решение. Если медь в каждом слое сплошная, температуру
можно разложить в ряд по косинусам вдоль платы, и для каждой гармоники
остаётся система 2 × 2. Сетка сходится к ряду: на плате 50 × 50 мм
с медью 35 мкм с двух сторон расхождение 0,74 % при шаге 0,5 мм, 0,19 %
при 0,25 мм и 0,05 % при 0,125 мм. Второй — та же плата раздела 131,
но матрица собрана заново по рёбрам через scipy.sparse и решена прямым
методом spsolve: все четыре варианта из 130б.6 совпали с сопряжёнными
градиентами до третьего знака. В каждом расчёте проверяется и баланс:
всё, что подано, уходит в воздух с точностью 10⁻⁶.
def exact(L, src, s1, s2, M=1500):
"""Средний перегрев прямоугольника src = (x0, y0, x1, y1) на ватт: ряд по cos(mπx/L)cos(nπy/L)."""
x0, y0, x1, y1 = src
m = np.arange(M)
km = m * math.pi / L
def integ(a, b):
out = np.empty(M)
out[0] = b - a
out[1:] = (np.sin(km[1:] * b) - np.sin(km[1:] * a)) / km[1:]
return out
ix, iy = integ(x0, x1), integ(y0, y1)
eps = np.where(m == 0, 1.0, 2.0)
area = (x1 - x0) * (y1 - y0)
q = (eps[:, None] * eps[None, :]) * np.outer(iy, ix) / (L * L) / area # P = 1 Вт
k2 = km[:, None] ** 2 + km[None, :] ** 2
g = KZ / TB
a11, a22 = s1 * k2 + H + g, s2 * k2 + H + g
t1 = q * a22 / (a11 * a22 - g * g)
return float(np.sum(t1 * np.outer(iy, ix)) / area)
Принятые числа сильнее всего сдвигает h. Неподвижный воздух — это не число из справочника, а диапазон, и его модель не уточняет: при h = 7, 10 и 14 Вт/(м²·К) у стабилизатора с медью 20 × 20 мм с двух сторон выходит 69,3, 57,6 и 48,7 К/Вт. Поэтому числа модели — для сравнения вариантов между собой, а запас по температуре — не меньше 20 %.
130б.3 Переходное отверстие и слой меди
Переходное отверстие — медная трубка: внутри пусто (или паяльная маска),
тепло идёт только по стенке. Её сечение — кольцо, и сопротивление
одного отверстия на длину платы 1,6 мм считается одной строкой
(via_r в листинге выше):
| Отверстие | Стенка 18 мкм | 25 мкм | 35 мкм |
|---|---|---|---|
| 0,2 мм | 399 К/Вт | 298 К/Вт | 226 К/Вт |
| 0,3 мм | 257 К/Вт | 190 К/Вт | 141 К/Вт |
| 0,4 мм | 190 К/Вт | 139 К/Вт | 102 К/Вт |
Одно отверстие — сотни К/Вт: само по себе это плохой теплоотвод. Но сравнить надо с тем, что вместо него: стеклотекстолит 1,6 мм под квадратом 1 × 1 мм — 5333 К/Вт. Одно отверстие 0,3 мм (как в правилах KiCad по умолчанию из раздела 130.6) проводит как 28 таких квадратов. Восемь отверстий под фланцем — 24 К/Вт параллельно, против сотен через FR-4 под той же площадкой.
Слой меди считают «на квадрат», как сопротивление плёнки: проводимость
квадрата не зависит от его размера. У меди 35 мкм это 0,0136 Вт/К —
в 10,7 раза больше, чем у всей толщи FR-4 1,6 мм вдоль платы; у 70 мкм —
0,0273 Вт/К, в 21,3 раза. Отсюда же длина, на которую тепло успевает
растечься, прежде чем уйдёт в воздух, √(S / 2h): около 27 мм для 35 мкм
и 37 мм для 70 мкм. Медь дальше этого расстояния от источника почти
не работает — это видно в следующей таблице.
130б.4 Площадь меди и переходные под SOT-223
Опыт: стабилизатор в SOT-223 посреди двусторонней платы 50 × 50 мм.
Тепло входит через фланец 2,0 × 3,8 мм (размеры площадки — из
посадочного места KiCad SOT-223-3_TabPin2; в сетке 0,25 мм она
получается 2,0 × 4,0 мм). Вокруг фланца — квадрат меди со стороной a.
Четыре варианта: медь только сверху, 35 и 70 мкм; сверху 35 мкм плюс
снизу сплошной полигон другой цепи без переходных; медь сверху и снизу
одного размера, соединённая восемью переходными во фланце.
| Медь a × a | Сверху 35 мкм | Сверху 70 мкм | + снизу сплошь, без переходных | Сверху и снизу a × a, 8 переходных |
|---|---|---|---|---|
| только фланец | 263,6 | 262,3 | 169,5 | 219,8 |
| 10 мм (100 мм²) | 112,7 | 106,1 | 65,0 | 100,8 |
| 15 мм (225 мм²) | 82,3 | 74,0 | 52,5 | 72,8 |
| 20 мм (400 мм²) | 66,3 | 56,9 | 47,4 | 57,6 |
| 30 мм (900 мм²) | 51,3 | 40,6 | 43,5 | 42,9 |
| 40 мм (1600 мм²) | 45,9 | 34,5 | 42,3 | 37,6 |
| 50 мм (вся плата) | 44,8 | 33,3 | 42,1 | 36,7 |
Числа — «фланец — воздух», К/Вт. Что из них следует:
- Первые сантиметры меди — главное. От одного фланца до квадрата 10 мм — с 263,6 до 112,7 К/Вт, больше чем вдвое. От 30 до 50 мм (почти втрое больше меди) — только с 51,3 до 44,8: дальше длины растекания медь не работает.
- Толстая медь помогает на большой площади. На голом фланце 70 мкм ничего не дают (263,6 и 262,3), на квадрате 30 мм — 40,6 вместо 51,3.
- Сплошной нижний слой работает и без переходных. Под 400 мм² меди стеклотекстолит 1,6 мм — это уже около 13 К/Вт поперёк, и тепло уходит в полигон, разлитый по всей плате: 47,4 против 66,3 К/Вт без него. Остров снизу размером с верхний квадрат с восемью переходными до 20 мм проигрывает сплошному слою — у того больше площадь; с 30 мм они на равных.
Сколько ставить переходных, зависит от того, есть ли вокруг фланца медь сверху. Два опыта на той же плате:
| Переходных во фланце и рядом | Медь 20 мм сверху и снизу | Сверху только фланец, снизу медь 30 мм |
|---|---|---|
| 0 | 63,3 | 175,4 |
| 1 | 61,8 | 130,2 |
| 2 | 60,7 | 109,9 |
| 4 | 59,2 | 89,3 |
| 8 | 57,6 | 71,6 |
| 16 | 56,7 | 69,6 |
| 32 | 56,2 | 68,6 |
Когда сверху есть свой полигон, переходные почти не нужны: 400 мм² верхней меди и так передают тепло вниз через FR-4. Когда сверху только площадка (у QFN с открытой площадкой, у фланца, зажатого чужими дорожками), переходные — единственный путь вниз: первое отверстие снимает 45 К/Вт, восемь — со 175,4 до 71,6. Дальше насыщение: вторые восемь дают 2 К/Вт, потому что теперь мешает растекание в нижней меди. Первые 8 — во фланце, следующие — по кругу вокруг него, шаг 1 мм:
def via_grid(ny, nx, pts):
v = np.zeros((ny, nx))
for x, y in pts:
v[int(y / D), int(x / D)] += 1
return v
def ldo(a, top_t=35e-6, bottom="none", nvia=0, h=H_AIR, b=None):
"""θ фланец–воздух, К/Вт: квадрат меди a × a сверху; снизу none / island (такой же квадрат или b × b)
/ plane (вся плата); nvia переходных — сначала во фланце, потом вокруг."""
top = TAB | rect(N, N, D, C - a / 2, C - a / 2, C + a / 2, C + a / 2)
b = a if b is None else b
isl = TAB | rect(N, N, D, C - b / 2, C - b / 2, C + b / 2, C + b / 2)
bot = {"none": ZERO, "island": isl, "plane": ~ZERO}[bottom]
th, bal, it = theta(D, [top, bot], [top_t, 35e-6], [1.6 * MM], via_grid(N, N, SITES[:nvia]), TAB, h=h)
return th, bal, it
130б.5 Сколько меди нужно на 0,5–1,5 Вт
Теперь — расчёт «наоборот». Предел кристалла 125 °C, «кристалл — фланец»
θJC для примера 15 К/Вт (это число из даташита конкретного стабилизатора —
подставляйте своё). Плате остаётся (125 − Tокр) / P − θJC. Из таблицы
130б.4 берём первый вариант, который проходит: сначала медь только
сверху, потом сверху и снизу с восемью переходными. Для 5 → 3,3 В
0,5 Вт — это около 0,3 А; для 12 → 3,3 В — меньше 0,06 А.
| P | Вокруг | Плате можно | Хватит | θ платы | Кристалл |
|---|---|---|---|---|---|
| 0,5 Вт | 40 °C | 155,0 К/Вт | квадрат 10 мм сверху | 112,7 К/Вт | 104 °C |
| 0,5 Вт | 85 °C | 65,0 К/Вт | квадрат 30 мм сверху | 51,3 К/Вт | 118 °C |
| 1,0 Вт | 40 °C | 70,0 К/Вт | квадрат 20 мм сверху | 66,3 К/Вт | 121 °C |
| 1,0 Вт | 85 °C | 25,0 К/Вт | не хватает даже 70 мкм на всю плату 50 × 50 мм (33,3) | — | 133 °C |
| 1,5 Вт | 40 °C | 41,7 К/Вт | квадрат 40 мм сверху и снизу, 8 переходных | 37,6 К/Вт | 119 °C |
| 1,5 Вт | 85 °C | 11,7 К/Вт | не хватает даже всей платы (33,3) | — | 157 °C |
Вывод тот же, что в 55.4, но теперь с площадью: полватта плата рассеивает почти даром, ватт в тёплом корпусе — уже вся плата 50 × 50 мм и всё равно мало, полтора ватта при 85 °C медью не решаются. Там радиатор (55.4) или импульсный преобразователь (53.4). Нужный «тепловой путь» для своих напряжений и тока считает вкладка «LDO» калькуляторов приложения (раздел 55.4); из таблицы 130б.4 видно, какая медь его даёт.
130б.6 Плата из раздела 131 и сверка с даташитами STM32
На плате из раздела 131 стабилизатор U2 — SOT-223, и фланец у этого
посадочного места — вывод 2, цепь +3V3. Сверху к нему подходит только
дорожка 0,5 мм к конденсатору C8; снизу — сплошная земля, разлитая на
1330,2 мм². Скрипт kicad130b.py в образе kicad/kicad:9.0 (KiCad
9.0.9) делает из этой платы ещё три варианта, заливает полигоны, гоняет
DRC и снимает растр меди обоих слоёв с шагом 0,25 мм — его читает модель:
def add_zone(b, layer, connection, priority):
z = pcbnew.ZONE(b)
z.SetLayer(layer)
z.SetNet(net(b, "+3V3"))
z.SetAssignedPriority(priority)
z.SetPadConnection(connection)
z.SetLocalClearance(mm(0.3))
z.Outline().NewOutline()
x0, y0, x1, y1 = ZONE
for x, y in ((x0, y0), (x1, y0), (x1, y1), (x0, y1)):
z.Outline().Append(mm(x), mm(y))
b.Add(z)
def variant(name):
"""Собрать вариант, сохранить, загрузить из файла и залить (как в разделе 131), вернуть путь."""
b = pcbnew.LoadBoard(SRC)
if name in ("relief", "full", "vias"):
conn = pcbnew.ZONE_CONNECTION_THERMAL if name == "relief" else pcbnew.ZONE_CONNECTION_FULL
add_zone(b, pcbnew.F_Cu, conn, 1)
if name == "vias":
add_zone(b, pcbnew.B_Cu, pcbnew.ZONE_CONNECTION_FULL, 1) # приоритет выше земли: остров в ней
for x, y in VIAS:
v = pcbnew.PCB_VIA(b)
v.SetPosition(VECTOR2I(mm(x), mm(y)))
v.SetDrill(mm(0.3))
v.SetWidth(mm(0.6))
v.SetNet(net(b, "+3V3"))
b.Add(v)
path = os.path.join(W, name + ".kicad_pcb")
pcbnew.SaveBoard(path, b)
shutil.copy(SRC.replace(".kicad_pcb", ".kicad_pro"), path.replace(".kicad_pcb", ".kicad_pro"))
b = pcbnew.LoadBoard(path)
b.BuildConnectivity()
pcbnew.ZONE_FILLER(b).Fill(b.Zones())
pcbnew.SaveBoard(path, b)
return path
def raster(b, layer):
"""Растр меди слоя (любой цепи): строки из '#' и '.', ячейка STEP мм, по центрам ячеек."""
bb = b.GetBoardEdgesBoundingBox()
nx, ny = int(round(ToMM(bb.GetWidth()) / STEP)), int(round(ToMM(bb.GetHeight()) / STEP))
g = [["."] * nx for _ in range(ny)]
items = [z for z in b.Zones() if z.IsOnLayer(layer)]
items += [p for fp in b.GetFootprints() for p in fp.Pads() if p.IsOnLayer(layer)]
items += [t for t in b.GetTracks() if t.IsOnLayer(layer)]
for it in items:
box = it.GetBoundingBox()
i0, i1 = max(0, int(ToMM(box.GetTop()) / STEP)), min(ny, int(ToMM(box.GetBottom()) / STEP) + 1)
j0, j1 = max(0, int(ToMM(box.GetLeft()) / STEP)), min(nx, int(ToMM(box.GetRight()) / STEP) + 1)
for i in range(i0, i1):
for j in range(j0, j1):
p = VECTOR2I(mm((j + 0.5) * STEP), mm((i + 0.5) * STEP))
if isinstance(it, pcbnew.ZONE):
hit = it.GetFilledPolysList(layer).Contains(p)
else:
hit = it.HitTest(p)
if hit:
g[i][j] = "#"
return ["".join(r) for r in g], nx, ny
| Вариант | Медь | DRC | Фланец — воздух | Кристалл при 0,5 Вт, 40 °C |
|---|---|---|---|---|
| как в 131 | фланец 7,60 мм² и дорожка | 0 нарушений, 24 неразведённых | 155,4 К/Вт | 125,2 °C |
полигон +3V3 сверху 14 × 10,5 мм, тепловые перемычки |
114,7 мм² | 0, 24 | 91,9 К/Вт | 93,5 °C |
| тот же полигон, к площадкам сплошь | 133,0 мм² | 0, 24 | 78,3 К/Вт | 86,7 °C |
плюс остров +3V3 снизу и 8 переходных во фланце |
133,0 + 147,0 мм² | 0, 24 | 72,9 К/Вт | 84,0 °C |
Кристалл посчитан как 40 + 0,5 × (15 + θ). Как есть, при 0,5 Вт
стабилизатор уже на пределе 125 °C; полигон на свободном месте вокруг
него снимает 32 градуса с перемычками и 39 — без них. Тепловые перемычки — их KiCad ставит по
умолчанию, чтобы площадку можно было пропаять, — стоят здесь
13,6 К/Вт. Остров снизу вырезан из земли (её стало 1165,2 мм²), и он
даёт немного: под полигоном и так лежала земля, через FR-4 тепло
уходило в неё. Неразведённых соединений во всех вариантах столько же,
сколько в разделе 131, — 24: остров не отрезал от земли ни одного
переходного.
Сверка с даташитами STM32. Модель не знает, что внутри корпуса, но
считает плату. Возьмём те же площадки корпуса (из посадочных мест KiCad
LQFP-48_7x7mm_P0.5mm и LQFP-100_14x14mm_P0.5mm) и положим их
посреди платы 76 × 114 мм — похожей на тестовые платы JEDEC; текст
JESD51 с разделом не сверялся, слои и их толщины приняты. «1s» —
один слой 70 мкм, только площадки; «2s2p» — плюс два сплошных
внутренних слоя по 35 мкм на глубине 0,5 и 1,1 мм. QFN-48 — открытая
площадка 5,6 × 5,6 мм, на 2s2p — без переходных и с девятью.
| Корпус и плата | Площадки — воздух, модель | θJA, даташит | Остаётся корпусу |
|---|---|---|---|
| LQFP-48, 1s | 109,0 К/Вт | 55 (F103) | — |
| LQFP-100, 1s | 61,2 К/Вт | 46 (F103), 43 (F407) | — |
| LQFP-48, 2s2p | 38,8 К/Вт | 55 (F103) | 16,2 К/Вт |
| LQFP-100, 2s2p | 23,1 К/Вт | 46 (F103), 43 (F407) | 22,9 и 19,9 К/Вт |
| QFN-48, 2s2p без переходных | 57,9 К/Вт | — | — |
| QFN-48, 2s2p, 9 переходных | 27,7 К/Вт | 32 (F103, UFQFPN48) | 4,3 К/Вт |
Честно о расхождении. На одной сигнальной плате модель даёт больше паспортного θJA ещё до всякого корпуса — значит, плата ST богаче медью, чем голые площадки: дорожки, полигоны, слои. С четырёхслойной платой числа сходятся по смыслу: корпусу LQFP остаётся 16–23 К/Вт, QFN с площадкой прямо под кристаллом — 4,3. Но разницу LQFP48 и UFQFPN48 модель даёт 11,1 К/Вт, а даташит — 23: на что именно ST паяла QFN, неизвестно, и подогнать это модель не может. Порядок верный, точности нет.
Отсюда оценка для своей платы, не больше чем оценка: «корпус» из четырёхслойного варианта плюс «плата» своя. На плате 131 площадки LQFP-48 к воздуху — 69,4 К/Вт: под ними нет своего полигона, а земля снизу за 1,6 мм FR-4. Тогда θJA F103 на ней — около 16,2 + 69,4 = 85,6 °C/Вт вместо 55. При 0,165 Вт худшего режима из 204.4 перегрев — 14,1 °C вместо 9,1 °C. Для микроконтроллера это всё ещё немного — главный печник на плате стабилизатор, — но запас «по даташиту» на чужой плате надо пересчитывать на свою.
130б.7 Грабли
Под фланец SOT-223 поставили переходные в землю — короткое замыкание.
Фланец SOT-223 — не обязательно земля: на плате 131 (посадочное место
SOT-223-3_TabPin2) это вывод 2, +3V3. Цепь фланца смотрят в даташите
своего стабилизатора. Переходные из фланца ведут только в медь той же
цепи: остров в нижнем слое (130б.6), а не сплошную землю.
Полигон есть, а стабилизатор греется. Площадка соединена с полигоном
тепловыми перемычками — так KiCad делает по умолчанию. На плате 131 это
91,9 К/Вт вместо 78,3. Для площадок, через которые уходит тепло, в
свойствах зоны или площадки ставят соединение «сплошь» (Solid).
θJA из даташита подставили как есть. Это число для платы производителя, которую STM32-даташиты даже не называют. На плате 131 модель даёт для F103 около 85,6 °C/Вт вместо 55. Для горячих деталей тепло считают по своей меди.
Залили медью полплаты — а лучше не стало. Медь дальше 27–37 мм от источника почти не работает: на плате 50 × 50 мм квадрат 50 мм вместо 30 мм дал 44,8 вместо 51,3 К/Вт. Нужна медь рядом, а не много меди.
Наставили 30 переходных — без толку. Если сверху вокруг площадки своя медь, переходные почти ничего не добавляют (63,3 → 56,2 К/Вт от 0 до 32). Они нужны там, где сверху только площадка: тогда первые восемь снимают больше 100 К/Вт.
Модель обещала 80 °C, а палец не держит. h = 10 Вт/(м²·К) — принятое число для неподвижного воздуха; в закрытой коробке меньше, и при h = 7 то же сопротивление уже на 20 % выше. Температуру платы проверяют термопарой или встроенным датчиком (204.4), а не только моделью.
130б.8 Итоги
- θJA — цепочка «кристалл — корпус — плата — воздух» на плате производителя; даташиты STM32 пишут только «JESD51-2, неподвижный воздух». LQFP48 и UFQFPN48 у F103 — 55 и 32 °C/Вт: разница — открытая площадка 5,6 × 5,6 мм, припаянная к плате.
- Модель: сетка 0,25 мм, медь 390, FR-4 0,30 и 0,80 Вт/(м·К),
h = 10 Вт/(м²·К); с точным рядом сходится до 0,19 %, со
spsolve— до третьего знака. - Переходное 0,3 мм со стенкой 25 мкм — 190 К/Вт, как 28 квадратов FR-4 по 1 × 1 мм. Медь 35 мкм растекает тепло на 27 мм, 70 мкм — на 37 мм.
- SOT-223 на плате 50 × 50 мм: фланец без меди 263,6 К/Вт, квадрат 10 мм — 112,7, 20 мм — 66,3, вся плата — 44,8; с медью 70 мкм — 33,3.
- Переходные нужны, когда сверху только площадка: 175,4 → 71,6 К/Вт от 0 до 8; при своей меди сверху — 63,3 → 57,6.
- 0,5 Вт при 85 °C — квадрат 30 мм; 1 Вт при 85 °C и 1,5 Вт при 85 °C медью платы 50 × 50 мм не решаются.
- Плата 131: U2 как есть 155,4 К/Вт (кристалл 125,2 °C при 0,5 Вт), с полигоном сплошь 78,3, с островом и 8 переходными 72,9; DRC — 0 нарушений во всех вариантах.
Полные исходники
ref130b.py — независимая сверка тепловой модели точным решением.
# -*- coding: utf-8 -*-
"""Раздел 130б: независимая сверка тепловой модели (numpy + scipy).
python3 ref130b.py /w
1. Точное решение для однородных слоёв: плата Lx × Ly с теплоизолированными краями, два слоя меди,
связанные через FR-4, тепло в воздух с двух сторон. Ряд по косинусам: для каждой гармоники —
система 2 × 2. Сравнение с сеткой heat130b при шаге 0,5; 0,25 и 0,125 мм.
2. Плата раздела 131 из растра KiCad: матрица собрана заново через scipy.sparse по рёбрам,
решение — прямым методом spsolve, сравнение с сопряжёнными градиентами heat130b."""
import math
import os
import sys
import numpy as np
import scipy
import scipy.sparse as sp
import scipy.sparse.linalg as sla
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import heat130b as hb
W = sys.argv[1] if len(sys.argv) > 1 else "."
MM = 1e-3
KCU, KZ, KXY, H = 390.0, 0.30, 0.80, 10.0 # те же принятые свойства, записаны заново
TB = 1.6e-3
def exact(L, src, s1, s2, M=1500):
"""Средний перегрев прямоугольника src = (x0, y0, x1, y1) на ватт: ряд по cos(mπx/L)cos(nπy/L)."""
x0, y0, x1, y1 = src
m = np.arange(M)
km = m * math.pi / L
def integ(a, b):
out = np.empty(M)
out[0] = b - a
out[1:] = (np.sin(km[1:] * b) - np.sin(km[1:] * a)) / km[1:]
return out
ix, iy = integ(x0, x1), integ(y0, y1)
eps = np.where(m == 0, 1.0, 2.0)
area = (x1 - x0) * (y1 - y0)
q = (eps[:, None] * eps[None, :]) * np.outer(iy, ix) / (L * L) / area # P = 1 Вт
k2 = km[:, None] ** 2 + km[None, :] ** 2
g = KZ / TB
a11, a22 = s1 * k2 + H + g, s2 * k2 + H + g
t1 = q * a22 / (a11 * a22 - g * g)
return float(np.sum(t1 * np.outer(iy, ix)) / area)
print("== 1. однородные слои: ряд против сетки, плата 50 × 50 мм, фланец в центре")
L = 50 * MM
for name, m2 in (("медь 35 мкм сверху", False), ("медь 35 мкм с двух сторон", True)):
s1 = KCU * 35e-6 + KXY * TB / 2
s2 = (KCU * 35e-6 if m2 else 0) + KXY * TB / 2
for d_mm in (0.5, 0.25, 0.125):
d = d_mm * MM
n = int(round(L / d))
tab = hb.rect(n, n, d, 24 * MM, 23.1 * MM, 26 * MM, 26.9 * MM)
ys, xs = np.nonzero(tab)
src = (xs.min() * d, ys.min() * d, (xs.max() + 1) * d, (ys.max() + 1) * d)
full = np.ones((n, n), bool)
fd, _, _ = hb.theta(d, [full, full if m2 else ~full], [35e-6, 35e-6], [TB], 0, tab)
ex = exact(L, src, s1, s2)
print("%s, шаг %.3f мм (источник %.2f × %.2f мм): ряд %.2f, сетка %.2f К/Вт, расхождение %.2f %%"
% (name, d_mm, (src[2] - src[0]) / MM, (src[3] - src[1]) / MM, ex, fd, 100 * (fd - ex) / ex))
print("== 2. плата 131: scipy.sparse + spsolve против CG")
lines = open(os.path.join(W, "mask130b.txt")).read().split("\n")
boards, k = {}, 0
while k < len(lines) and lines[k].startswith("variant"):
_, name, nx, ny, step = lines[k].split()
nx, ny, step = int(nx), int(ny), float(step) * MM
lay = [np.array([[c == "#" for c in r] for r in lines[k + 1 + l * ny:k + 1 + (l + 1) * ny]]) for l in (0, 1)]
vias = [tuple(float(v) * MM for v in p.split(",")) for p in lines[k + 1 + 2 * ny].split()[1:]]
k += 2 + 2 * ny
while k < len(lines) and lines[k].startswith(("tab", "mcu", "lqfp100")):
if lines[k].startswith("tab"):
tabr = [float(v) * MM for v in lines[k].split()[1:]]
k += 1
boards[name] = (lay, vias, nx, ny, step)
def sparse_theta(lay, vias, nx, ny, d, srcr):
"""Матрица теплопроводностей по рёбрам сетки и прямое решение."""
idx = lambda l, i, j: (l * ny + i) * nx + j
nn = 2 * ny * nx
S = [KCU * 35e-6 * lay[l] + KXY * TB / 2 for l in (0, 1)]
rows, cols, vals = [], [], []
diag = np.zeros(nn)
def edge(a, b, g):
g = np.broadcast_to(g, a.shape)
rows.extend([a, b])
cols.extend([b, a])
vals.extend([-g, -g])
np.add.at(diag, a, g)
np.add.at(diag, b, g)
I, J = np.mgrid[0:ny, 0:nx]
for l in (0, 1):
r = 1 / (2 * S[l][:, :-1]) + 1 / (2 * S[l][:, 1:])
edge(idx(l, I[:, :-1], J[:, :-1]).ravel(), idx(l, I[:, 1:], J[:, 1:]).ravel(), (1 / r).ravel())
r = 1 / (2 * S[l][:-1, :]) + 1 / (2 * S[l][1:, :])
edge(idx(l, I[:-1, :], J[:-1, :]).ravel(), idx(l, I[1:, :], J[1:, :]).ravel(), (1 / r).ravel())
nv = np.zeros((ny, nx))
for x, y in vias:
nv[int(y / d), int(x / d)] += 1
ring = math.pi * ((0.15 * MM) ** 2 - (0.125 * MM) ** 2)
gz = KZ * d * d / TB + nv * KCU * ring / TB
edge(idx(0, I, J).ravel(), idx(1, I, J).ravel(), gz.ravel())
diag += H * d * d
A = sp.coo_matrix((np.concatenate(vals + [diag]), (np.concatenate(rows + [np.arange(nn)]),
np.concatenate(cols + [np.arange(nn)]))), shape=(nn, nn)).tocsr()
yc, xc = (np.arange(ny) + 0.5) * d, (np.arange(nx) + 0.5) * d
src = (yc[:, None] >= srcr[1]) & (yc[:, None] < srcr[3]) & (xc[None, :] >= srcr[0]) & (xc[None, :] < srcr[2])
q = np.zeros(nn)
q[idx(0, *np.nonzero(src))] = 1.0 / src.sum()
T = sla.spsolve(A, q)
return float(T[idx(0, *np.nonzero(src))].mean())
for name in ("as131", "relief", "full", "vias"):
lay, vias, nx, ny, d = boards[name]
print("U2 на плате 131, %-6s: spsolve %.3f К/Вт" % (name, sparse_theta(lay, vias, nx, ny, d, tabr)))
print("numpy %s, scipy %s" % (np.__version__, scipy.__version__))
study130b.py — все опыты тепловой модели.
# -*- coding: utf-8 -*-
"""Раздел 130б: все опыты тепловой модели (heat130b.py).
python3 study130b.py /w (/w — каталог с mask130b.txt от kicad130b.py)
1. Одно переходное отверстие и слой меди: числа без сетки.
2. Стабилизатор в SOT-223 посреди платы 50 × 50 мм: площадь меди сверху, 35 и 70 мкм, медь снизу
с переходными во фланце, земля снизу без переходных; число переходных; коэффициент h.
3. Плата из раздела 131 (растр меди из KiCad): U2 как есть, полигон с перемычками и без, переходные.
4. Микроконтроллер: LQFP-48 и QFN-48 на платах 76 × 114 мм (один слой; четыре слоя) и на плате 131.
5. Сколько меди нужно на 0,5–1,5 Вт при 40 и 85 °C вокруг."""
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from heat130b import (H_AIR, K_CU, K_FR4_XY, K_FR4_Z, T_PLATE, rect, theta, via_r)
W = sys.argv[1] if len(sys.argv) > 1 else "."
MM = 1e-3
D = 0.25 * MM # шаг сетки
TJ_MAX, THETA_JC = 125.0, 15.0 # предел кристалла и «кристалл — фланец» для примера (принято)
def via_grid(ny, nx, pts):
v = np.zeros((ny, nx))
for x, y in pts:
v[int(y / D), int(x / D)] += 1
return v
print("== 1. переходное отверстие и слой меди")
print("принято: медь %.0f, FR-4 поперёк %.2f, вдоль %.2f Вт/(м·К); h = %.0f Вт/(м2·К) с каждой стороны; стенка %.0f мкм"
% (K_CU, K_FR4_Z, K_FR4_XY, H_AIR, T_PLATE * 1e6))
for drill in (0.2, 0.3, 0.4):
for pl in (18, 25, 35):
print("переходное %.1f мм, стенка %d мкм, плата 1,6 мм: %.0f К/Вт" % (drill, pl, via_r(drill * MM, pl * 1e-6)))
r_fr4 = 1.6 * MM / (K_FR4_Z * 1 * MM * 1 * MM)
print("FR-4 1,6 мм поперёк под квадратом 1 × 1 мм: %.0f К/Вт; одно переходное 0,3 мм заменяет %.0f таких квадратов"
% (r_fr4, r_fr4 / via_r(0.3 * MM)))
for t in (35, 70):
s = K_CU * t * 1e-6 + K_FR4_XY * 1.6 * MM / 2
print("медь %d мкм: проводимость на квадрат %.4f Вт/К, это FR-4 1,6 мм вдоль x%.1f; длина растекания sqrt(S/2h) %.0f мм"
% (t, K_CU * t * 1e-6, K_CU * t * 1e-6 / (K_FR4_XY * 1.6 * MM), np.sqrt(s / (2 * H_AIR)) / MM))
print("== 2. SOT-223 посреди платы 50 × 50 мм, 2 слоя, 1,6 мм, сетка 0,25 мм; источник — фланец 2,0 × 3,8 мм")
N = 200
C = 25 * MM
TAB = rect(N, N, D, C - 1.0 * MM, C - 1.9 * MM, C + 1.0 * MM, C + 1.9 * MM)
IN_PAD = sorted([(C + dx * MM, C + dy * MM) for dx in (-0.5, 0.5) for dy in (-1.5, -0.5, 0.5, 1.5)],
key=lambda p: (abs(p[0] - C) + abs(p[1] - C), p[1], p[0]))
OUT = sorted([(C + (i + 0.5) * MM, C + (j + 0.5) * MM) for i in range(-6, 6) for j in range(-6, 6)
if not (-1 <= i <= 0 and -2 <= j <= 1)],
key=lambda p: ((p[0] - C) ** 2 + (p[1] - C) ** 2, p[1], p[0]))
SITES = IN_PAD + OUT
ys, xs = np.nonzero(TAB)
print("фланец в сетке: %.2f × %.2f мм, %d ячеек" % ((np.ptp(xs) + 1) * D / MM, (np.ptp(ys) + 1) * D / MM, TAB.sum()))
ZERO = np.zeros((N, N), bool)
def ldo(a, top_t=35e-6, bottom="none", nvia=0, h=H_AIR, b=None):
"""θ фланец–воздух, К/Вт: квадрат меди a × a сверху; снизу none / island (такой же квадрат или b × b)
/ plane (вся плата); nvia переходных — сначала во фланце, потом вокруг."""
top = TAB | rect(N, N, D, C - a / 2, C - a / 2, C + a / 2, C + a / 2)
b = a if b is None else b
isl = TAB | rect(N, N, D, C - b / 2, C - b / 2, C + b / 2, C + b / 2)
bot = {"none": ZERO, "island": isl, "plane": ~ZERO}[bottom]
th, bal, it = theta(D, [top, bot], [top_t, 35e-6], [1.6 * MM], via_grid(N, N, SITES[:nvia]), TAB, h=h)
return th, bal, it
AREAS = (0, 10, 15, 20, 30, 40, 50)
TABLE = {}
for a in AREAS:
row = []
for key, kw in (("top35", {}), ("top70", {"top_t": 70e-6}), ("plane", {"bottom": "plane"}),
("island8", {"bottom": "island", "nvia": 8})):
th, bal, it = ldo(a * MM, **kw)
TABLE[key, a] = th
row.append("%s %.1f" % (key, th))
assert abs(bal - 1) < 1e-6
print("медь %2d мм (%4d мм2): %s К/Вт" % (a, a * a, " | ".join(row)))
print("баланс: во всех расчётах в воздух ушло P с точностью 1e-6")
for n in (0, 1, 2, 4, 8, 16, 32):
th, _, _ = ldo(20 * MM, bottom="island", nvia=n)
print("медь 20 мм сверху и снизу, переходных %2d: %.1f К/Вт" % (n, th))
for n in (0, 1, 2, 4, 8, 16, 32):
th, _, _ = ldo(0, bottom="island", nvia=n, b=30 * MM)
print("сверху только фланец, снизу медь 30 мм, переходных %2d: %.1f К/Вт" % (n, th))
for h in (7.0, 10.0, 14.0):
t0, _, _ = ldo(0, h=h)
t1, _, _ = ldo(20 * MM, bottom="island", nvia=8, h=h)
print("h = %4.1f: только фланец %.1f К/Вт, медь 20 мм с двух сторон и 8 переходных %.1f К/Вт" % (h, t0, t1))
print("== 3. плата из раздела 131 (растр KiCad 0,25 мм)")
lines = open(os.path.join(W, "mask130b.txt")).read().split("\n")
MASKS, VIAS, PADS = {}, {}, {}
i = 0
while i < len(lines) and lines[i]:
name, nx, ny, step = lines[i].split()[1:]
nx, ny = int(nx), int(ny)
assert abs(float(step) * MM - D) < 1e-12
top = np.array([[c == "#" for c in r] for r in lines[i + 1:i + 1 + ny]])
bot = np.array([[c == "#" for c in r] for r in lines[i + 1 + ny:i + 1 + 2 * ny]])
MASKS[name] = (top, bot)
i += 1 + 2 * ny
VIAS[name] = [tuple(float(v) * MM for v in p.split(",")) for p in lines[i].split()[1:]]
i += 1
while i < len(lines) and lines[i].startswith(("tab ", "mcu ", "lqfp100 ")):
key, *vals = lines[i].split()
if key == "tab":
TABR = [float(v) * MM for v in vals]
else:
PADS[key] = [[float(v) * MM for v in p.split(",")] for p in vals]
i += 1
NY, NX = MASKS["as131"][0].shape
SRC131 = rect(NY, NX, D, *TABR)
MCU131 = np.zeros((NY, NX), bool)
for r in PADS["mcu"]:
MCU131 |= rect(NY, NX, D, *r)
B131 = {}
for name in ("as131", "relief", "full", "vias"):
top, bot = MASKS[name]
th, bal, it = theta(D, [top, bot], [35e-6, 35e-6], [1.6 * MM], via_grid(NY, NX, VIAS[name]), SRC131)
B131[name] = th
print("U2 на плате 131, %-6s: %.3f К/Вт (переходных на плате %d, шагов CG %d)" % (name, th, len(VIAS[name]), it))
top, bot = MASKS["as131"]
th131, _, _ = theta(D, [top, bot], [35e-6, 35e-6], [1.6 * MM], via_grid(NY, NX, VIAS["as131"]), MCU131)
print("U1 LQFP-48 на плате 131 (источник — 48 площадок): %.1f К/Вт" % th131)
print("== 4. микроконтроллер на платах 76 × 114 мм, сетка 0,25 мм")
RY, RX = 456, 304
cx, cy = RX * D / 2, RY * D / 2
def centered(rects):
"""Маска площадок корпуса, перенесённых в центр опорной платы."""
mx = (min(r[0] for r in rects) + max(r[2] for r in rects)) / 2
my = (min(r[1] for r in rects) + max(r[3] for r in rects)) / 2
m = np.zeros((RY, RX), bool)
for r in rects:
m |= rect(RY, RX, D, r[0] - mx + cx, r[1] - my + cy, r[2] - mx + cx, r[3] - my + cy)
return m
EP = rect(RY, RX, D, cx - 2.8 * MM, cy - 2.8 * MM, cx + 2.8 * MM, cy + 2.8 * MM)
EPV = [(cx + i * 1.5 * MM, cy + j * 1.5 * MM) for i in (-1, 0, 1) for j in (-1, 0, 1)]
Z, F = np.zeros((RY, RX), bool), np.ones((RY, RX), bool)
REF = {}
for pk, cu, boards in (("LQFP-48", centered(PADS["mcu"]), ("1s", "2s2p")), ("LQFP-100", centered(PADS["lqfp100"]), ("1s", "2s2p")),
("QFN-48", EP, ("2s2p", "2s2p+9"))):
for board in boards:
if board == "1s":
th, _, _ = theta(D, [cu, Z], [70e-6, 35e-6], [1.6 * MM], 0, cu)
else:
v = via_grid(RY, RX, EPV) if board == "2s2p+9" else 0
th, _, _ = theta(D, [cu, F, F, Z], [70e-6, 35e-6, 35e-6, 70e-6], [0.5 * MM, 0.6 * MM, 0.5 * MM], v, cu)
REF[pk, board] = th
print("%s, плата %-6s: площадки–воздух %.1f К/Вт" % (pk, board, th))
print("модель, 1s: LQFP-48 %.1f, LQFP-100 %.1f К/Вт — больше паспортных θJA без всякого корпуса"
% (REF["LQFP-48", "1s"], REF["LQFP-100", "1s"]))
for pk, chip, ds in (("LQFP-48", "F103", 55.0), ("LQFP-100", "F103", 46.0), ("LQFP-100", "F407", 43.0),
("QFN-48", "F103", 32.0)):
b = "2s2p+9" if pk == "QFN-48" else "2s2p"
print("%s %s: даташит %.0f, плата 2s2p в модели %.1f, корпусу остаётся %.1f К/Вт" % (chip, pk, ds, REF[pk, b], ds - REF[pk, b]))
print("разница LQFP48 и UFQFPN48: даташит F103 %.0f, модель 2s2p %.1f К/Вт"
% (55 - 32, REF["LQFP-48", "2s2p"] - REF["QFN-48", "2s2p+9"]))
rest = 55.0 - REF["LQFP-48", "2s2p"]
print("F103 LQFP-48 на плате 131: θJA ≈ %.1f + %.1f = %.1f °C/Вт; при 0,165 Вт: +%.1f °C вместо +%.1f по даташиту"
% (rest, th131, rest + th131, 0.165 * (rest + th131), 0.165 * 55))
print("== 5. сколько меди нужно: θJC = %.0f К/Вт (принято), Tj не выше %.0f °C" % (THETA_JC, TJ_MAX))
ORDER = [("top35", a) for a in AREAS] + [("island8", a) for a in AREAS]
NAMES = {"top35": "сверху %d мм", "island8": "сверху и снизу %d мм + 8 переходных"}
for P in (0.5, 1.0, 1.5):
for ta in (40.0, 85.0):
need = (TJ_MAX - ta) / P - THETA_JC
pick = next(((k, a) for k, a in ORDER if TABLE[k, a] <= need), None)
if pick:
k, a = pick
tj = ta + P * (THETA_JC + TABLE[k, a])
print("P = %.1f Вт, вокруг %.0f °C: нужно θ платы <= %.1f К/Вт -> %s (%.1f К/Вт), Tj = %.0f °C"
% (P, ta, need, NAMES[k] % a, TABLE[k, a], tj))
else:
best = min(TABLE.values())
print("P = %.1f Вт, вокруг %.0f °C: нужно θ платы <= %.1f К/Вт -> не хватает даже всей платы (%.1f К/Вт), Tj = %.0f °C"
% (P, ta, need, best, ta + P * (THETA_JC + best)))
print("numpy", np.__version__)
kicad130b.py — четыре варианта меди под стабилизатором на плате раздела 131 в KiCad и растр меди для модели.
# -*- coding: utf-8 -*-
"""Раздел 130б: медь под стабилизатором на плате из раздела 131 — в KiCad 9 (образ kicad/kicad:9.0).
python3 kicad130b.py /p/good.kicad_pcb /w
Берёт исправленную плату раздела 131 и делает четыре варианта узла питания вокруг U2 (SOT-223,
фланец — вывод 2, цепь +3V3):
as131 — как есть: фланец, дорожка 0,5 мм к C8, снизу — сплошная земля;
relief — сверху полигон +3V3 14 × 10,5 мм, к площадкам — тепловыми перемычками (как по умолчанию);
full — тот же полигон, к площадкам — сплошь;
vias — full плюс такой же полигон +3V3 снизу (остров в земле) и 8 переходных во фланце.
Для каждого: заливка, площади, DRC через kicad-cli, растр меди обоих слоёв с шагом 0,25 мм
в /w/mask130b.txt — его читает тепловая модель (study130b.py)."""
import json
import os
import shutil
import subprocess
import sys
import pcbnew
from pcbnew import VECTOR2I, FromMM as mm, ToMM
SRC, W = sys.argv[1], sys.argv[2]
STEP = 0.25 # мм, шаг растра
ZONE = (9.0, 21.0, 23.0, 31.5) # мм, прямоугольник полигона +3V3 вокруг U2
VIAS = [(x, y) for x in (16.65, 17.65) for y in (24.5, 25.5, 26.5, 27.5)] # во фланце 2,0 × 3,8 мм
def net(b, name):
return b.GetNetInfo().GetNetItem(name)
def add_zone(b, layer, connection, priority):
z = pcbnew.ZONE(b)
z.SetLayer(layer)
z.SetNet(net(b, "+3V3"))
z.SetAssignedPriority(priority)
z.SetPadConnection(connection)
z.SetLocalClearance(mm(0.3))
z.Outline().NewOutline()
x0, y0, x1, y1 = ZONE
for x, y in ((x0, y0), (x1, y0), (x1, y1), (x0, y1)):
z.Outline().Append(mm(x), mm(y))
b.Add(z)
def variant(name):
"""Собрать вариант, сохранить, загрузить из файла и залить (как в разделе 131), вернуть путь."""
b = pcbnew.LoadBoard(SRC)
if name in ("relief", "full", "vias"):
conn = pcbnew.ZONE_CONNECTION_THERMAL if name == "relief" else pcbnew.ZONE_CONNECTION_FULL
add_zone(b, pcbnew.F_Cu, conn, 1)
if name == "vias":
add_zone(b, pcbnew.B_Cu, pcbnew.ZONE_CONNECTION_FULL, 1) # приоритет выше земли: остров в ней
for x, y in VIAS:
v = pcbnew.PCB_VIA(b)
v.SetPosition(VECTOR2I(mm(x), mm(y)))
v.SetDrill(mm(0.3))
v.SetWidth(mm(0.6))
v.SetNet(net(b, "+3V3"))
b.Add(v)
path = os.path.join(W, name + ".kicad_pcb")
pcbnew.SaveBoard(path, b)
shutil.copy(SRC.replace(".kicad_pcb", ".kicad_pro"), path.replace(".kicad_pcb", ".kicad_pro"))
b = pcbnew.LoadBoard(path)
b.BuildConnectivity()
pcbnew.ZONE_FILLER(b).Fill(b.Zones())
pcbnew.SaveBoard(path, b)
return path
def raster(b, layer):
"""Растр меди слоя (любой цепи): строки из '#' и '.', ячейка STEP мм, по центрам ячеек."""
bb = b.GetBoardEdgesBoundingBox()
nx, ny = int(round(ToMM(bb.GetWidth()) / STEP)), int(round(ToMM(bb.GetHeight()) / STEP))
g = [["."] * nx for _ in range(ny)]
items = [z for z in b.Zones() if z.IsOnLayer(layer)]
items += [p for fp in b.GetFootprints() for p in fp.Pads() if p.IsOnLayer(layer)]
items += [t for t in b.GetTracks() if t.IsOnLayer(layer)]
for it in items:
box = it.GetBoundingBox()
i0, i1 = max(0, int(ToMM(box.GetTop()) / STEP)), min(ny, int(ToMM(box.GetBottom()) / STEP) + 1)
j0, j1 = max(0, int(ToMM(box.GetLeft()) / STEP)), min(nx, int(ToMM(box.GetRight()) / STEP) + 1)
for i in range(i0, i1):
for j in range(j0, j1):
p = VECTOR2I(mm((j + 0.5) * STEP), mm((i + 0.5) * STEP))
if isinstance(it, pcbnew.ZONE):
hit = it.GetFilledPolysList(layer).Contains(p)
else:
hit = it.HitTest(p)
if hit:
g[i][j] = "#"
return ["".join(r) for r in g], nx, ny
def pad_rects(fp, num=None):
"""Прямоугольники площадок посадочного места (x0, y0, x1, y1), мм; num — только площадки с этим номером."""
rs = []
for p in fp.Pads():
if num is None or p.GetNumber() == num:
c, s = p.GetPosition(), p.GetSize()
rs.append(tuple(round(ToMM(v), 3) for v in (c.x - s.x // 2, c.y - s.y // 2, c.x + s.x // 2, c.y + s.y // 2)))
return rs
out = open(os.path.join(W, "mask130b.txt"), "w")
for name in ("as131", "relief", "full", "vias"):
path = variant(name)
b = pcbnew.LoadBoard(path)
areas = {}
for z in b.Zones():
key = "%s %s" % (z.GetNetname(), "F.Cu" if z.IsOnLayer(pcbnew.F_Cu) else "B.Cu")
areas[key] = areas.get(key, 0) + z.GetFilledArea() / 1e12
rep = os.path.join(W, name + "_drc.json")
subprocess.run(["kicad-cli", "pcb", "drc", "--severity-all", "--format", "json", "-o", rep, path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
drc = json.load(open(rep))
kinds = sorted(set(v["type"] for v in drc["violations"]))
nvia = sum(1 for t in b.GetTracks() if t.Type() == pcbnew.PCB_VIA_T)
print("%-6s заливка: %s | нарушений DRC %d %s | неразведённых %d | переходных %d" % (
name, ", ".join("%s %.1f мм2" % (k, v) for k, v in sorted(areas.items())),
len(drc["violations"]), kinds, len(drc["unconnected_items"]), nvia))
top, nx, ny = raster(b, pcbnew.F_Cu)
bot, _, _ = raster(b, pcbnew.B_Cu)
out.write("variant %s %d %d %.3f\n" % (name, nx, ny, STEP))
out.write("\n".join(top) + "\n" + "\n".join(bot) + "\n")
vias = sorted((ToMM(t.GetPosition().x), ToMM(t.GetPosition().y)) for t in b.GetTracks() if t.Type() == pcbnew.PCB_VIA_T)
out.write("vias " + " ".join("%.3f,%.3f" % v for v in vias) + "\n")
if name == "as131":
tab = max(pad_rects(b.FindFootprintByReference("U2"), "2"), key=lambda r: (r[2] - r[0]) * (r[3] - r[1]))
out.write("tab %.3f %.3f %.3f %.3f\n" % tab)
print("U2 фланец (вывод 2, +3V3): %.2f × %.2f мм, %.2f мм2" % (tab[2] - tab[0], tab[3] - tab[1],
(tab[2] - tab[0]) * (tab[3] - tab[1])))
for key, fp in (("mcu", b.FindFootprintByReference("U1")),
("lqfp100", pcbnew.FootprintLoad("/usr/share/kicad/footprints/Package_QFP.pretty",
"LQFP-100_14x14mm_P0.5mm"))):
rs = pad_rects(fp)
out.write(key + " " + " ".join("%.3f,%.3f,%.3f,%.3f" % r for r in rs) + "\n")
print("%s %s: площадок %d, каждая %.3f × %.3f мм" % (key, fp.GetFPIDAsString().split(":")[-1], len(rs),
rs[0][2] - rs[0][0], rs[0][3] - rs[0][1]))
print("%-6s медь в растре %.2f мм: F.Cu %.1f мм2, B.Cu %.1f мм2" % (
name, STEP, sum(r.count("#") for r in top) * STEP * STEP, sum(r.count("#") for r in bot) * STEP * STEP))
out.close()
print("KiCad", pcbnew.Version())
Дальше: раздел 131 «KiCad» — эта же плата, собранная скриптом и проверенная.