GCC Code Coverage Report


Directory: ./
File: non-gpl/BBShiftPanel/BBShiftPanel.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 38 1437 2.6%
Functions: 8 70 11.4%
Branches: 23 1805 1.3%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the CC-BY-ND as described in the
10 * included LICENSE.CC-BY-ND file. This file may be modified for
11 * personal use, but modified copies MAY NOT be redistributed in any form.
12 */
13
14 #include "fpp-pch.h"
15
16 #include "fpp-json.h"
17
18 #include <pthread.h>
19 #include <sched.h>
20 #include <sys/mman.h>
21 #include <sys/wait.h>
22 #include <time.h>
23 #include <tuple>
24 #include <unistd.h>
25
26 #include <algorithm>
27 #include <chrono>
28 #include <cmath>
29 #include <cstdlib>
30 #include <cstring>
31 #include <set>
32
33 // FPP includes
34 #include "../../Sequence.h"
35 #include "../../Warnings.h"
36 #include "channeloutput/ChannelOutputSetup.h"
37 #include "../../common.h"
38 #include "../../log.h"
39 #include "../../settings.h"
40
41 #include "BBShiftPanel.h"
42 #include "../../channeloutput/GammaLUT.h"
43 #include "../CapeUtils/CapeUtils.h"
44 #include "../../pru/SMEMRing.hp"
45 #include "util/BBBUtils.h"
46
47 #include "MapPixelsByDepth16.h"
48
49 #include "channeloutput/PanelInterleaveHandler.h"
50 #include "overlays/PixelOverlay.h"
51
52 #include "Plugin.h"
53 class BBShiftPanelPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin {
54 public:
55 28 BBShiftPanelPlugin() :
56
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
84 FPPPlugins::Plugin("BBShiftPanel") {
57 28 }
58 28 virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override {
59
2/6
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 28 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
56 return new BBShiftPanelOutput(startChannel, channelCount);
60 }
61 };
62
63 extern "C" {
64 28 FPPPlugins::Plugin* createPlugin() {
65
2/6
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 28 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
56 return new BBShiftPanelPlugin();
66 }
67 }
68
69 constexpr int ADDRESSING_MODE_STANDARD = 0;
70 constexpr int ADDRESSING_MODE_AB_SHIFT = 1; // row shift register, A=clock, B=data (active low)
71 constexpr int ADDRESSING_MODE_DIRECT = 2;
72 constexpr int ADDRESSING_MODE_ABC_SHIFT = 3; // row shift register, A=clock, C=data (active high)
73 constexpr int ADDRESSING_MODE_ABC_DE = 4; // SM5266P: A/B/C drive a shifter, D/E select the bank
74 constexpr int ADDRESSING_MODE_FM6353C = 50;
75 constexpr int ADDRESSING_MODE_FM6363C = 51;
76 constexpr int ADDRESSING_MODE_FM6373 = 52;
77 constexpr int ADDRESSING_MODE_DP3364 = 53;
78 constexpr int ADDRESSING_MODE_ICND1065L = 54;
79 constexpr int ADDRESSING_MODE_SM16380SH = 55;
80 constexpr int ADDRESSING_MODE_ICND2153 = 56;
81
82 constexpr int PANEL_TYPE_FM6126A = 1;
83 constexpr int PANEL_TYPE_FM6127 = 2;
84 // The PWM chips are presented in the UI as panel types (they are driver
85 // chips like the FM6126A), but internally they are PWM addressing modes
86 constexpr int PANEL_TYPE_FM6363C = 3;
87 constexpr int PANEL_TYPE_FM6353 = 4;
88 constexpr int PANEL_TYPE_FM6373 = 5; // the common DP32019B receiver boards
89 constexpr int PANEL_TYPE_DP3364 = 6;
90 constexpr int PANEL_TYPE_ICND1065L = 7;
91 constexpr int PANEL_TYPE_SM16380SH = 8;
92 // Split out of PANEL_TYPE_FM6353, which used to claim ICN2153 compatibility.
93 // The two take the same register grammar but not the same values, so an
94 // existing config on type 4 keeps the FM6353 payload it has always had.
95 constexpr int PANEL_TYPE_ICND2153 = 9;
96
97 // must match PWM_CHIP_CONFIG_OFFSET in BBShiftPanel_pwm.asm
98 #define PWM_CHIP_CONFIG_OFFSET 0x1DF8
99 // the register slots live at the front of the same PRU data RAM, so growing
100 // them must not reach the chip config word
101 static_assert(offsetof(BBShiftPanelData, registers) + sizeof(((BBShiftPanelData*)nullptr)->registers) <= PWM_CHIP_CONFIG_OFFSET);
102 // REG1_OFF in BBShiftPanel_pwm.asm is this offset, hardcoded as 16
103 static_assert(offsetof(BBShiftPanelData, registers) == 16);
104
105 // FM6373 config register sequence, one {address, value} word per register,
106 // different per color line. Captured from a working DP32019B 128x64 panel
107 // by the kingdo9/rpi-rgb-led-matrix_pwm_experiment project
108 // (lib/spwm-panel-registers.cc, register config 0); DMD_STM32 carries a
109 // shorter single-color variant of the same address space. The 0x02
110 // register holds the scan row count and is patched at runtime.
111 static const uint16_t FM6373_SEQ_R[] = {
112 0x0000, 0x0100, 0x021f, 0x033f, 0x0402, 0x0508, 0x0602, 0x0720,
113 0x0820, 0x0900, 0x0a00, 0x0b00, 0x0c01, 0x0d01, 0x0e04, 0x0f01,
114 0x10c2, 0x1121, 0x1201, 0x1300, 0x1400, 0x1500, 0x1600, 0x17f0,
115 0x181f, 0x1900, 0x1a1f, 0x1b10, 0x1c2a, 0x1d0a, 0x1e42, 0x1f04,
116 0x2008, 0x2101, 0x221c, 0x7000, 0x7100, 0x7200, 0x7300, 0x7400,
117 0xf000, 0xf100, 0xf200, 0xf300, 0xf400, 0xf500, 0x2300
118 };
119 static const uint16_t FM6373_SEQ_G[] = {
120 0x0000, 0x0100, 0x021f, 0x033f, 0x0402, 0x0508, 0x0602, 0x0720,
121 0x0820, 0x0900, 0x0a00, 0x0b00, 0x0c08, 0x0d01, 0x0e04, 0x0f01,
122 0x10c2, 0x1121, 0x1201, 0x1300, 0x1400, 0x1500, 0x1600, 0x17f0,
123 0x181f, 0x1950, 0x1a1f, 0x1b10, 0x1c2a, 0x1d0a, 0x1e46, 0x1f20,
124 0x2008, 0x2101, 0x221c, 0x7000, 0x7100, 0x7200, 0x7300, 0x7400,
125 0xf000, 0xf100, 0xf200, 0xf300, 0xf400, 0xf500, 0x2300
126 };
127 static const uint16_t FM6373_SEQ_B[] = {
128 0x0000, 0x0100, 0x021f, 0x033f, 0x0402, 0x0508, 0x0602, 0x0720,
129 0x0820, 0x0900, 0x0a00, 0x0b00, 0x0c08, 0x0d01, 0x0e04, 0x0f01,
130 0x10c2, 0x1121, 0x1201, 0x1300, 0x1400, 0x1500, 0x1600, 0x17f0,
131 0x182f, 0x1900, 0x1a1f, 0x1b10, 0x1c2a, 0x1d0a, 0x1e48, 0x1f20,
132 0x2010, 0x2101, 0x221c, 0x7000, 0x7100, 0x7200, 0x7300, 0x7400,
133 0xf000, 0xf100, 0xf200, 0xf300, 0xf400, 0xf500, 0x2300
134 };
135 constexpr int FM6373_SEQ_LEN = 47;
136 static_assert(sizeof(FM6373_SEQ_R) == FM6373_SEQ_LEN * sizeof(uint16_t));
137 static_assert(sizeof(FM6373_SEQ_G) == FM6373_SEQ_LEN * sizeof(uint16_t));
138 static_assert(sizeof(FM6373_SEQ_B) == FM6373_SEQ_LEN * sizeof(uint16_t));
139
140 // FM6373 registers for a full height 1/64 scan panel, from the same project
141 // (fm6373.profiles, fm6373_regtype2, a 128x64 capture). Nine registers differ
142 // from the 1/32 table above beyond the scan count, and the whole 0x70/0xf0
143 // tail block moves to a repeated 0x54 - which is why patching only the scan
144 // count does not turn a 1/32 table into a working 1/64 one.
145 static constexpr uint16_t FM6373_64S_SEQ_R[] = {
146 0x0000, 0x0100, 0x023f, 0x033f, 0x0402, 0x0508, 0x0602, 0x0710,
147 0x0810, 0x0900, 0x0a00, 0x0b00, 0x0c01, 0x0d03, 0x0e02, 0x0f11,
148 0x10c2, 0x1121, 0x1201, 0x1300, 0x1400, 0x1500, 0x1600, 0x17f0,
149 0x181f, 0x1900, 0x1a1f, 0x1b10, 0x1cbe, 0x1d0e, 0x1e42, 0x1f24,
150 0x2008, 0x2101, 0x221c, 0x5400, 0x5400, 0x5400, 0x5400, 0x5400,
151 0x5403, 0x5403, 0x5403, 0x5403, 0x5403, 0x5403, 0x2300
152 };
153 static constexpr uint16_t FM6373_64S_SEQ_G[] = {
154 0x0000, 0x0100, 0x023f, 0x033f, 0x0402, 0x0508, 0x0602, 0x0710,
155 0x0810, 0x0900, 0x0a00, 0x0b00, 0x0c08, 0x0d03, 0x0e04, 0x0f11,
156 0x10c2, 0x1121, 0x1201, 0x1300, 0x1400, 0x1500, 0x1600, 0x17f0,
157 0x181f, 0x1950, 0x1a1f, 0x1b10, 0x1cbe, 0x1d0e, 0x1e46, 0x1f20,
158 0x2008, 0x2101, 0x221c, 0x5400, 0x5400, 0x5400, 0x5400, 0x5400,
159 0x5403, 0x5403, 0x5403, 0x5403, 0x5403, 0x5403, 0x2300
160 };
161 static constexpr uint16_t FM6373_64S_SEQ_B[] = {
162 0x0000, 0x0100, 0x023f, 0x033f, 0x0402, 0x0508, 0x0602, 0x0710,
163 0x0810, 0x0900, 0x0a00, 0x0b00, 0x0c08, 0x0d01, 0x0e04, 0x0f11,
164 0x10c2, 0x1121, 0x1201, 0x1300, 0x1400, 0x1500, 0x1600, 0x17f0,
165 0x182f, 0x1900, 0x1a1f, 0x1b10, 0x1cbe, 0x1d0e, 0x1e48, 0x1f20,
166 0x2010, 0x2101, 0x221c, 0x5400, 0x5400, 0x5400, 0x5400, 0x5400,
167 0x5403, 0x5403, 0x5403, 0x5403, 0x5403, 0x5403, 0x2300
168 };
169
170 // ICND1065L config registers. From kingdo9/rpi-rgb-led-matrix_pwm_experiment
171 // (lib/spwm/registertest/data/icnd1065l.profiles, icnd1065l_regtype1 - the
172 // project's built-in default, captured from a 1/43 scan panel). The chip
173 // takes the same upload grammar as the FM6373, so only the payload differs.
174 static constexpr uint16_t ICND1065L_SEQ_R[] = {
175 0x0000, 0x026a, 0x0322, 0x0412, 0x0500, 0x0601, 0x0712, 0x0c10,
176 0x0d02, 0x0e84, 0x0f01, 0x1040, 0x1127, 0x1800, 0x1926, 0x1c60,
177 0x1d02, 0x1e71, 0x2040, 0x2101, 0x2380, 0x74a0
178 };
179 static constexpr uint16_t ICND1065L_SEQ_G[] = {
180 0x0000, 0x026a, 0x0322, 0x0412, 0x0500, 0x0601, 0x0712, 0x0c10,
181 0x0d04, 0x0e84, 0x0f01, 0x1040, 0x1127, 0x1800, 0x1908, 0x1c60,
182 0x1d02, 0x1e92, 0x2060, 0x2101, 0x2305, 0x74a0
183 };
184 static constexpr uint16_t ICND1065L_SEQ_B[] = {
185 0x0000, 0x026a, 0x0322, 0x0412, 0x0500, 0x0601, 0x0712, 0x0c10,
186 0x0d03, 0x0e84, 0x0f11, 0x1040, 0x1127, 0x1800, 0x190a, 0x1c60,
187 0x1d02, 0x1eb5, 0x2060, 0x2101, 0x2300, 0x74a0
188 };
189 constexpr int ICND1065L_SEQ_LEN = 22;
190
191 // SM16380SH config registers, same source (spwm-panel-registers.cc built-in
192 // main block, a 1/32 scan capture). This chip takes a sixth slot carrying
193 // 0xF003 ahead of the commit pair, and skips the FM6373's middle LAT burst.
194 static constexpr uint16_t SM16380SH_SEQ_R[] = {
195 0x021f, 0x0300, 0x0400, 0x0500, 0x0600, 0x078c, 0x0800, 0x0900,
196 0x0a02, 0x0b0c, 0x0c08, 0x0d00, 0x0e05, 0x0f00, 0x1000, 0x1100,
197 0x1200, 0x1308, 0x1414, 0x1500, 0x1630, 0x1700, 0x1801, 0x1904,
198 0x1a03, 0x1b14, 0x1c12, 0x1d00, 0x1e00, 0x1f0c, 0x2000, 0x2200
199 };
200 static constexpr uint16_t SM16380SH_SEQ_G[] = {
201 0x021f, 0x0300, 0x0400, 0x0500, 0x0600, 0x078c, 0x0800, 0x0900,
202 0x0a02, 0x0b0c, 0x0c18, 0x0d00, 0x0e05, 0x0f00, 0x1000, 0x1100,
203 0x1200, 0x1308, 0x1422, 0x1500, 0x1630, 0x1700, 0x1801, 0x1903,
204 0x1a01, 0x1b14, 0x1c8f, 0x1d00, 0x1e00, 0x1f0c, 0x2000, 0x2200
205 };
206 static constexpr uint16_t SM16380SH_SEQ_B[] = {
207 0x021f, 0x0300, 0x0400, 0x0500, 0x0600, 0x078c, 0x0800, 0x0900,
208 0x0a02, 0x0b0c, 0x0c30, 0x0d00, 0x0e05, 0x0f00, 0x1000, 0x1100,
209 0x1200, 0x1308, 0x1432, 0x1500, 0x1630, 0x1700, 0x1801, 0x1903,
210 0x1a01, 0x1b14, 0x1c8f, 0x1d00, 0x1e00, 0x1f0c, 0x2000, 0x2200
211 };
212 constexpr int SM16380SH_SEQ_LEN = 32;
213
214 // The FM6373, ICND1065L and SM16380SH uploads are one grammar: a few bare LAT
215 // bursts, then N 16-bit slots each latched over their last 5 clocks, with slot
216 // 3 rotating through the chip's config sequence one word per frame. They
217 // differ only in the payload, whether the middle (11 clock) LAT burst is sent,
218 // how many slots follow, and which register carries the scan count.
219 // A capture taken at one scan rate. Registers other than the scan count
220 // change with the scan rate on these chips, so a table is only really valid
221 // for the geometry it came from; where a second capture is known the closest
222 // one is used instead of stretching the default.
223 // PWMChipSeq / PWMChipSeqVariant are declared in BBShiftPanel.h so the
224 // manager can hold an imported profile by value.
225
226 ✗ static const PWMChipSeq* pwmChipSeqFor(int addressingMode) {
227 // Scan count: every profile in the kingdo9 catalog puts (rows - 1) in the
228 // low 6 bits of this register and leaves the upper 2 bits as a chip
229 // constant, so the patch preserves whatever the table above carries there
230 // (ICND1065L sets bit 6; the other two leave it clear).
231 static const PWMChipSeqVariant FM6373_VARIANTS[] = {
232 { 64, FM6373_64S_SEQ_R, FM6373_64S_SEQ_G, FM6373_64S_SEQ_B },
233 };
234 static const PWMChipSeq FM6373_SEQ = {
235 FM6373_SEQ_R, FM6373_SEQ_G, FM6373_SEQ_B, FM6373_SEQ_LEN, 5, 0, true, 0x02,
236 32, FM6373_VARIANTS, 1
237 };
238 static const PWMChipSeq ICND1065L_SEQ = {
239 ICND1065L_SEQ_R, ICND1065L_SEQ_G, ICND1065L_SEQ_B, ICND1065L_SEQ_LEN, 5, 0, true, 0x02,
240 43, nullptr, 0
241 };
242 static const PWMChipSeq SM16380SH_SEQ = {
243 SM16380SH_SEQ_R, SM16380SH_SEQ_G, SM16380SH_SEQ_B, SM16380SH_SEQ_LEN, 6, 0xF003, false, 0x02,
244 32, nullptr, 0
245 };
246 ✗ switch (addressingMode) {
247 ✗ case ADDRESSING_MODE_FM6373:
248 ✗ return &FM6373_SEQ;
249 ✗ case ADDRESSING_MODE_ICND1065L:
250 ✗ return &ICND1065L_SEQ;
251 ✗ case ADDRESSING_MODE_SM16380SH:
252 ✗ return &SM16380SH_SEQ;
253 }
254 ✗ return nullptr;
255 }
256
257 // DP3364S config registers, one {address, value} word per register, different
258 // per color line. Captured from a working 128x64 1/64 scan panel and posted
259 // to hzeller/rpi-rgb-led-matrix issue #1821; the chip's own datasheet
260 // documents the access protocol but not the register map.
261 //
262 // The datasheet says 15 valid register addresses and that 15 frames complete
263 // a full refresh, which is exactly 0x02-0x0F plus 0x15 - the addresses the
264 // capture carries values for (0x10-0x14 all read back zero and are taken as
265 // reserved). Documented meanings, for the ones that are documented:
266 // 0x03[6:0] PWM display packet count - 1 (0x3f = 64, the async mode default)
267 // 0x04[6:0] row PWM display length - 1
268 // 0x06[2:0] internal GCLK multiplier, FGCLK = FDCLK * (n + 1)
269 // 0x08[7:0] linear output current multiplier - the brightness knob, patched
270 // at runtime; the captured 0x7f is what the panel was built for,
271 // so that is full brightness and FPP only scales down from it
272 // 0x0b[5] 1.5x current gain
273 // 0x0c[7:6] PWM display mode (01 = high gray data independent refresh, the
274 // free running row scan this driver generates); [1] = drop open
275 // circuit bad spots
276 // 0x0f[6:0] current reference
277 // 0x02 is undocumented but holds 0x3f on a 64 row panel and is patched at
278 // runtime with the scan count, the same as 0x02 on the FM6373.
279 static constexpr uint16_t DP3364_SEQ_R[] = {
280 0x023f, 0x033f, 0x041a, 0x0504, 0x0639, 0x0700, 0x087f, 0x0968,
281 0x0abe, 0x0b28, 0x0c58, 0x0d08, 0x0e08, 0x0f20, 0x1500
282 };
283 static constexpr uint16_t DP3364_SEQ_G[] = {
284 0x023f, 0x033f, 0x041a, 0x0504, 0x0639, 0x070c, 0x087f, 0x096b,
285 0x0abf, 0x0b2b, 0x0c58, 0x0d12, 0x0e0b, 0x0f20, 0x1504
286 };
287 static constexpr uint16_t DP3364_SEQ_B[] = {
288 0x023f, 0x033f, 0x041a, 0x0500, 0x0639, 0x070c, 0x087f, 0x0961,
289 0x0abe, 0x0b31, 0x0c58, 0x0d18, 0x0e01, 0x0f20, 0x1504
290 };
291 constexpr int DP3364_SEQ_LEN = 15;
292 // setupGCLKConfig reads the group count out of the 0x03 entry
293 static_assert((DP3364_SEQ_R[1] >> 8) == 0x03);
294 static_assert(sizeof(DP3364_SEQ_R) == DP3364_SEQ_LEN * sizeof(uint16_t));
295 static_assert(sizeof(DP3364_SEQ_G) == DP3364_SEQ_LEN * sizeof(uint16_t));
296 static_assert(sizeof(DP3364_SEQ_B) == DP3364_SEQ_LEN * sizeof(uint16_t));
297
298 // Approximate DCLK period in nanoseconds for the two PWM firmware variants,
299 // from the instruction counts in BBShiftPanel_pwm.asm at the AM62x PRU's 4ns
300 // cycle: OUTPUT_PIXEL is 24 cycles for 8 outputs and 42 for 16, plus a ~26
301 // cycle LOAD_DATA every 8 (resp. 4) pixels.
302 constexpr int DCLK_NS_8 = 109;
303 constexpr int DCLK_NS_16 = 194;
304
305 // Minimum DCLK periods the DP3364S needs per displayed row, from the "time of
306 // one line" formula the datasheet repeats for each PWM display mode:
307 // (2*(reg0x05[7:4]+1) + 2*(reg0x05[3:0]+1) + 4*(reg0x04[6:0]+1))
308 // / (reg0x06[2:0]+1)
309 // The three color lines are separate chips and need not agree, so the scan has
310 // to hold for the slowest of them.
311 ✗ static int dp3364MinLineDCLKs(const uint16_t* seq, int len) {
312 ✗ int r04 = 0, r05 = 0, r06 = 0;
313 ✗ for (int i = 0; i < len; i++) {
314 ✗ switch (seq[i] >> 8) {
315 ✗ case 0x04:
316 ✗ r04 = seq[i] & 0x7F;
317 ✗ break;
318 ✗ case 0x05:
319 ✗ r05 = seq[i] & 0xFF;
320 ✗ break;
321 ✗ case 0x06:
322 ✗ r06 = seq[i] & 0x07;
323 ✗ break;
324 }
325 }
326 ✗ return (2 * (((r05 >> 4) & 0x0F) + 1) + 2 * ((r05 & 0x0F) + 1) + 4 * (r04 + 1)) / (r06 + 1);
327 }
328
329 constexpr int PWM_COMMAND_SYNC = 0x0001;
330 constexpr int PWM_COMMAND_REGISTERS = 0x0002;
331 constexpr int PWM_COMMAND_STARTGCLK = 0x0004;
332 constexpr int PWM_COMMAND_DATA = 0x0008;
333 constexpr int PWM_COMMAND_HALT = 0xFFFF;
334
335 // Note: the ordering of the bit planes within a frame (formerly a set of
336 // hand-tuned BIT_ORDERS tables) is now computed by buildStrideSchedule(),
337 // which also splits the long MSB on-times into multiple pulses spread
338 // across the frame to raise the perceived refresh rate.
339
340 static const std::vector<std::string> PRU_PINS = { "P1-20", "P1-29", "P1-31", "P1-33", "P1-36",
341 "P2-02", "P2-04", "P2-06", "P2-08",
342 "P2-18", "P2-20", "P2-22", "P2-24",
343 "P2-28", "P2-30", "P2-32", "P2-33",
344 "P2-34", "P2-35" };
345
346 static const std::vector<std::string> PRU0_PWM_PINS = {
347 "P1-31", "P2-28", "P2-30", "P2-32", "P2-34", "P1-36"
348 };
349
350 constexpr int MAX_OUTPUTS = 16;
351
352 // how much the pump thread copies into the shared memory ring at a time; a
353 // multiple of 48 and 8 (the ring write splits blocks across the wrap point,
354 // so it does not need to divide the ring size evenly)
355 constexpr uint32_t PUMP_BLOCK_SIZE = 3072;
356
357 BBShiftPanelManager BBShiftPanelManager::INSTANCE;
358
359 28 BBShiftPanelOutput::BBShiftPanelOutput(unsigned int startChannel, unsigned int channelCount) :
360 28 ChannelOutput(startChannel, channelCount) {
361
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 LogDebug(VB_CHANNELOUT, "BBShiftPanelOutput::BBShiftPanelOutput(%u, %u)\n",
362 startChannel, channelCount);
363 28 }
364
365 ✗ void BBShiftPanelOutput::StartingOutput() {
366 // StoppingOutput is also called when the channel output thread simply
367 // goes idle, so the stopping flag has to clear when output resumes
368 ✗ m_stopping = false;
369 ✗ BBShiftPanelManager::INSTANCE.startingOutput();
370 ✗ }
371
372 ✗ void BBShiftPanelManager::startingOutput() {
373 ✗ std::lock_guard<std::mutex> memberLock(m_memberLock);
374 ✗ if (m_activeMembers++) {
375 // the cape is already refreshing for another matrix
376 ✗ return;
377 }
378 // Restart the refresh before the init registers go out below: the PRU
379 // has to be running to see them at all, and it only reads commands at a
380 // frame boundary, so if it is still working through a frame it needs the
381 // pump running to get there.
382 ✗ UnparkPRUs();
383 ✗ if (!isPWMPanel() && m_panelType && pruData) {
384 // FM6126A style panels need their configuration registers clocked
385 // out before they will display anything; the registers can be lost
386 // if the panels lose power, so resend them whenever output starts
387 ✗ sendPanelInitPackets();
388 }
389 ✗ }
390
391 // must match PANEL_INIT_OFFSET / ADDR_CONFIG_OFFSET in BBShiftPanel.asm
392 #define PANEL_INIT_OFFSET 0x1E00
393 #define ADDR_CONFIG_OFFSET 0x1DF8
394
395 ✗ void BBShiftPanelManager::sendPanelInitPackets() {
396 // panel configuration register writes, from the rpi-rgb-led-matrix
397 // project (lib/framebuffer.cc InitFM6126/InitFM6127): a register write
398 // is the pattern repeated across the whole chain width on every data
399 // line with LATCH held through the last (leCount-1) column clocks.
400 // Pattern bit i is the data value for column i mod 16.
401 auto encode = [](const char* p) {
402 uint16_t v = 0;
403 for (int i = 0; i < 16; i++) {
404 if (p[i] == '1') {
405 v |= 1 << i;
406 }
407 }
408 return v;
409 };
410 struct InitReg {
411 uint16_t leCount;
412 uint16_t pattern;
413 };
414 ✗ std::vector<InitReg> regs;
415 ✗ if (m_panelType == PANEL_TYPE_FM6126A) {
416 // FM6126A: config register 1 (full brightness), register 2 (panel on)
417 ✗ regs.push_back({ 12, encode("0111111111111111") });
418 ✗ regs.push_back({ 13, encode("0000000001000000") });
419 ✗ } else if (m_panelType == PANEL_TYPE_FM6127) {
420 // FM6127: registers 1-3
421 ✗ regs.push_back({ 12, encode("1111111111001110") });
422 ✗ regs.push_back({ 13, encode("1110000001100010") });
423 ✗ regs.push_back({ 11, encode("0101111100000000") });
424 } else {
425 ✗ return;
426 }
427 // the PRU data RAM cannot take a plain memcpy (uncacheable segment,
428 // SIGBUS on aarch64) - memcpyToPRU handles it
429 ✗ uint32_t buf[16] = { 0 };
430 ✗ buf[0] = (uint32_t)regs.size() | (((uint32_t)rowLen) << 16);
431 ✗ for (size_t i = 0; i < regs.size(); i++) {
432 ✗ buf[i + 1] = (uint32_t)regs[i].leCount | (((uint32_t)regs[i].pattern) << 16);
433 }
434 ✗ pru->memcpyToPRU(pru->data_ram + PANEL_INIT_OFFSET, (uint8_t*)buf, sizeof(buf));
435 ✗ __sync_synchronize();
436 ✗ pruData->command = 0xFFF0;
437 ✗ int cnt = 0;
438 ✗ while (pruData->command == 0xFFF0 && cnt < 250) {
439 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
440 ✗ cnt++;
441 }
442 ✗ if (pruData->command == 0xFFF0) {
443 ✗ LogWarn(VB_CHANNELOUT, "BBShiftPanel: panel init registers were not sent (PRU did not respond)\n");
444 } else {
445 ✗ LogDebug(VB_CHANNELOUT, "BBShiftPanel: sent %zu panel init registers for panel type %d\n", regs.size(), m_panelType);
446 }
447 ✗ }
448
449 ✗ void BBShiftPanelOutput::StoppingOutput() {
450 // Called when the channel output thread stops and before the output is
451 // deleted on a config reload; the thread may still be inside
452 // PrepData/SendData, so refuse new work and wait for any in-flight call
453 // to drain so a teardown cannot race them.
454 ✗ m_stopping = true;
455 ✗ while (m_inFlight > 0) {
456 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
457 }
458 ✗ BBShiftPanelManager::INSTANCE.stoppingOutput();
459 ✗ }
460
461 ✗ void BBShiftPanelManager::stoppingOutput() {
462 ✗ std::lock_guard<std::mutex> memberLock(m_memberLock);
463 ✗ if (m_activeMembers > 0 && --m_activeMembers) {
464 // another matrix on this cape is still outputting
465 ✗ return;
466 }
467 // a partially arrived or prepped frame must not carry across the park, or
468 // the first frame after the restart would publish stale content
469 ✗ m_preppedThisFrame = 0;
470 ✗ m_framePrepped = false;
471 ✗ ParkPRUs();
472 ✗ }
473
474 // The channel output thread only stops once nothing wants to output at all
475 // (forceOutput() in channeloutputthread.cpp covers sequences, overlays,
476 // effects, testing and bridging), and a shift panel only holds an image for
477 // as long as the pump keeps re-shifting it - an idle panel is simply dark.
478 // Streaming that blank frame costs most of a core for as long as fppd runs,
479 // so park instead:
480 // - the pump stops on a frame boundary and clears the command word
481 // - the PRU reads that at the end of the frame it is in and parks in its
482 // command wait loop, which holds the display off; the OE PRU parks the
483 // same way once the brightness handshake stops
484 // - both cores are then halted, which also freezes their cycle counters
485 // That last part is not just about saving (already idle) PRU cycles: the
486 // counter saturates after 2^32 cycles (~17s) and the hardware clears CTR_EN
487 // when it does, which RESET_PRU_CLOCK cannot undo, and the OE PRU's
488 // GET_PRU_CLOCK spin loops would then never exit. A halted core cannot
489 // reach that state.
490 //
491 // PWM panels refresh from their own registers and their pump already idles
492 // between commands, so none of this applies to them.
493 ✗ void BBShiftPanelManager::ParkPRUs() {
494 ✗ if (isPWMPanel() || !pru || !pruData || m_prusPaused) {
495 ✗ return;
496 }
497 ✗ m_pumpPaused = true;
498 // The pump only acts on this at a frame boundary, so it has written
499 // whole frames only.
500 ✗ int cnt = 0;
501 ✗ while (m_pumpRunning && !m_pumpParked && cnt < 500) {
502 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
503 ✗ cnt++;
504 }
505 // Then the frame still in flight has to drain, so the PRU ends up on the
506 // same frame boundary the pump stopped on and the two stay aligned in
507 // the byte stream. The ring is far smaller than a frame, so this is at
508 // most that frame's tail.
509 ✗ for (cnt = 0; !m_ring.drained() && cnt < 500; cnt++) {
510 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
511 }
512 ✗ if (!m_pumpParked || !m_ring.drained()) {
513 // never leave the cores halted somewhere unknown - a frozen mid-scan
514 // output would hold one row lit
515 ✗ LogWarn(VB_CHANNELOUT, "BBShiftPanel: could not park (pumpParked=%d drained=%d), leaving the refresh running\n",
516 (int)m_pumpParked, (int)m_ring.drained());
517 ✗ m_pumpPaused = false;
518 ✗ return;
519 }
520 // The OE PRU only leaves its wait loop when this PRU hands it a
521 // brightness, so with this PRU parked one more display period is enough
522 // for both to be sitting in their command wait loops with the display
523 // off. Halting anywhere else would freeze the pins mid-scan.
524 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(10));
525 ✗ pru->pause();
526 ✗ if (pwmPru) {
527 ✗ pwmPru->pause();
528 }
529 ✗ m_prusPaused = true;
530 }
531
532 ✗ void BBShiftPanelManager::UnparkPRUs() {
533 ✗ if (m_prusPaused) {
534 // the OE PRU first, so it is already waiting by the time the panel
535 // PRU starts handing it brightnesses
536 ✗ if (pwmPru) {
537 ✗ pwmPru->resume();
538 }
539 ✗ if (pru) {
540 ✗ pru->resume();
541 }
542 ✗ m_prusPaused = false;
543 }
544 ✗ m_pumpPaused = false;
545 ✗ }
546
547 112 BBShiftPanelOutput::~BBShiftPanelOutput() {
548 56 LogDebug(VB_CHANNELOUT, "BBShiftPanelOutput::~BBShiftPanelOutput()\n");
549
550 56 m_stopping = true;
551
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
56 while (m_inFlight > 0) {
552 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
553 }
554 // covers an Init that failed after the manager accepted this matrix, where
555 // the output is deleted without Close() ever being called
556 56 releaseToManager();
557 56 BBShiftPanelManager::INSTANCE.removeMember(this);
558
559
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
56 if (m_matrix)
560 ✗ delete m_matrix;
561
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
56 if (m_panelMatrix)
562 ✗ delete m_panelMatrix;
563 112 }
564
565 28 void BBShiftPanelManager::removeMember(BBShiftPanelOutput* m) {
566
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 std::lock_guard<std::mutex> memberLock(m_memberLock);
567
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 auto it = std::find(m_members.begin(), m_members.end(), m);
568
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 if (it == m_members.end()) {
569 // never registered (its Init failed)
570 28 return;
571 }
572 ✗ m_members.erase(it);
573 ✗ m_preppedThisFrame = 0;
574 ✗ m_framePrepped = false;
575 // The remaining members keep the geometry they were mapped against. It
576 // stays valid - a rowLen longer than the remaining chains need just shifts
577 // a few extra pixels off the end of those chains - and rebuilding here
578 // would restart the PRU in the middle of a shutdown.
579 ✗ if (m_members.empty()) {
580 ✗ teardown();
581 }
582
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28 }
583
584 // Stops the hardware but keeps the parsed configuration, so a rebuild can
585 // bring it back up with new geometry.
586 ✗ void BBShiftPanelManager::teardownHardware() {
587 ✗ stopBackgroundThreads();
588 ✗ StopPRU();
589 ✗ m_pwmPruStalled = false;
590 ✗ if (!m_pwmStallWarning.empty()) {
591 ✗ WarningHolder::RemoveWarning(m_pwmStallWarning);
592 ✗ m_pwmStallWarning.clear();
593 }
594 ✗ if (m_heapBuffers) {
595 ✗ for (auto& b : outputBuffers) {
596 ✗ if (b) {
597 ✗ free(b);
598 ✗ b = nullptr;
599 }
600 }
601 ✗ m_heapBuffers = false;
602 }
603 ✗ m_frontBuffer = nullptr;
604 ✗ currOutputBuffer = 0;
605 ✗ }
606
607 ✗ void BBShiftPanelManager::teardown() {
608 ✗ teardownHardware();
609 ✗ if (currentChannelData) {
610 ✗ delete[] currentChannelData;
611 ✗ currentChannelData = nullptr;
612 }
613 ✗ if (!m_refreshWarning.empty()) {
614 ✗ WarningHolder::RemoveWarning(m_refreshWarning);
615 ✗ m_refreshWarning.clear();
616 }
617 ✗ m_activeMembers = 0;
618 ✗ m_openMembers = 0;
619 ✗ }
620
621 ✗ bool BBShiftPanelManager::isPWMPanel() const {
622 ✗ return m_addressingMode >= ADDRESSING_MODE_FM6353C;
623 }
624
625 28 int BBShiftPanelOutput::Init(Json::Value config) {
626
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 LogDebug(VB_CHANNELOUT, "BBShiftPanelOutput::Init(JSON)\n");
627
628
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 Json::Value root;
629
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
28 std::string subType = config["configName"].asString();
630
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 int outputs = CapeUtils::INSTANCE.getLicensedOutputs();
631
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
28 if (!CapeUtils::INSTANCE.getPanelConfig(subType, root)) {
632
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 LogErr(VB_CHANNELOUT, "Could not read panel pin configuration for %s\n", subType.c_str());
633 28 return 0;
634 }
635
636 // Everything below here is this matrix's own: its pixel mapping, its
637 // channel range, its color handling. The hardware the frame goes out
638 // on is shared with any other matrix on the same cape and is set up by
639 // the manager in addMember() below.
640 ✗ m_invertedData = config["invertedData"].asInt();
641 ✗ m_colorOrder = ColorOrderFromString(config["colorOrder"].asString());
642 ✗ if (config.isMember("brightness")) {
643 ✗ m_brightness = config["brightness"].asInt();
644 }
645 ✗ if (m_brightness < 1 || m_brightness > 10) {
646 ✗ m_brightness = 10;
647 }
648 ✗ m_gamma = 2.2;
649 ✗ if (config.isMember("gamma")) {
650 ✗ m_gamma = atof(config["gamma"].asString().c_str());
651 }
652
653 ✗ int panelWidth = config["panelWidth"].asInt();
654 ✗ int panelHeight = config["panelHeight"].asInt();
655 ✗ if (!panelWidth) {
656 ✗ panelWidth = 32;
657 }
658 ✗ if (!panelHeight) {
659 ✗ panelHeight = 16;
660 }
661
662 ✗ m_panelMatrix = new PanelMatrix(panelWidth, panelHeight, m_invertedData);
663 ✗ if (!m_panelMatrix) {
664 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanelOutput: Unable to create PanelMatrix\n");
665 ✗ return 0;
666 }
667 ✗ for (int i = 0; i < config["panels"].size(); i++) {
668 ✗ Json::Value p = config["panels"][i];
669 ✗ if (p["outputNumber"].asInt() <= outputs) {
670 ✗ char orientation = 'N';
671 ✗ std::string o = p["orientation"].asString();
672
673 ✗ if (!o.empty()) {
674 ✗ orientation = o[0];
675 }
676
677 ✗ if (p["colorOrder"].asString() == "") {
678 ✗ p["colorOrder"] = ColorOrderToString(m_colorOrder);
679 }
680
681 // a refused panel (out of range) must not count toward the outputs or chains below
682 ✗ if (!m_panelMatrix->AddPanel(p["outputNumber"].asInt(),
683 ✗ p["panelNumber"].asInt(),
684 orientation,
685 ✗ p["xOffset"].asInt(), p["yOffset"].asInt(),
686 ✗ ColorOrderFromString(p["colorOrder"].asString()))) {
687 ✗ continue;
688 }
689 ✗ if (p["panelNumber"].asInt() > m_longestChain) {
690 ✗ m_longestChain = p["panelNumber"].asInt();
691 }
692 ✗ }
693 ✗ }
694 ✗ m_longestChain++;
695
696 // get the dimensions of the matrix
697 ✗ m_panels = m_panelMatrix->PanelCount();
698 ✗ m_width = m_panelMatrix->Width();
699 ✗ m_height = m_panelMatrix->Height();
700
701 ✗ m_channelCount = m_width * m_height * 3;
702
703 ✗ m_matrix = new Matrix(m_startChannel, m_width, m_height);
704 ✗ if (config.isMember("subMatrices")) {
705 ✗ for (int i = 0; i < config["subMatrices"].size(); i++) {
706 ✗ Json::Value sm = config["subMatrices"][i];
707
708 ✗ m_matrix->AddSubMatrix(
709 ✗ sm["enabled"].asInt(),
710 ✗ sm["startChannel"].asInt() - 1,
711 ✗ sm["width"].asInt(),
712 ✗ sm["height"].asInt(),
713 ✗ sm["xOffset"].asInt(),
714 ✗ sm["yOffset"].asInt());
715 ✗ }
716 }
717
718 // Claims the outputs this matrix drives on the shared cape and brings the
719 // PRU up (or rebuilds it around the new geometry if another matrix is
720 // already running). This is what builds the gamma table and the scatter
721 // map, since both depend on the shared frame layout.
722 ✗ if (!BBShiftPanelManager::INSTANCE.addMember(this, config, root)) {
723 ✗ return 0;
724 }
725 ✗ m_registered = true;
726
727 ✗ if (PixelOverlayManager::INSTANCE.isAutoCreatePixelOverlayModels()) {
728 ✗ std::string dd = "LED Panels";
729 ✗ if (config.isMember("LEDPanelMatrixName") && !config["LEDPanelMatrixName"].asString().empty()) {
730 ✗ dd = config["LEDPanelMatrixName"].asString();
731 }
732 ✗ if (config.isMember("description")) {
733 ✗ dd = config["description"].asString();
734 }
735 ✗ std::string desc = dd;
736 ✗ int count = 0;
737 ✗ while (PixelOverlayManager::INSTANCE.getModel(desc) != nullptr) {
738 ✗ count++;
739 ✗ desc = dd + "-" + std::to_string(count);
740 }
741 ✗ PixelOverlayManager::INSTANCE.addAutoOverlayModel(desc,
742 m_startChannel, m_channelCount, 3,
743 ✗ "H", m_invertedData ? "BL" : "TL",
744 ✗ m_height, 1);
745 ✗ m_autoCreatedModelName = desc;
746 ✗ }
747
748 ✗ return ChannelOutput::Init(config);
749 28 }
750
751 ✗ bool BBShiftPanelOutput::usesOutput(int o) const {
752 ✗ return m_panelMatrix && o >= 0 && o < MAX_MATRIX_OUTPUTS &&
753 ✗ !m_panelMatrix->m_outputPanels[o].empty();
754 }
755
756 // The settings below describe how the PRU shifts a frame out, so they belong
757 // to the cape rather than to any one matrix on it. The first matrix to
758 // register fixes them; checkCompatible() holds later ones to the same values.
759 ✗ BBShiftPanelManager::PanelParams BBShiftPanelManager::parsePanelParams(const Json::Value& config, const Json::Value& capeConfig) {
760 ✗ PanelParams p;
761 ✗ p.panelWidth = config["panelWidth"].asInt();
762 ✗ p.panelHeight = config["panelHeight"].asInt();
763 ✗ if (!p.panelWidth) {
764 ✗ p.panelWidth = 32;
765 }
766 ✗ if (!p.panelHeight) {
767 ✗ p.panelHeight = 16;
768 }
769
770 ✗ p.addressingMode = config["panelRowAddressType"].asInt();
771 ✗ p.panelType = config["panelType"].asInt();
772 // For PWM panel types the addressing dropdown selects how the GCLK
773 // program drives the row lines: Direct Row Select = binary row number
774 // on SEL0-4, anything else = the DP32020A style row shift register
775 ✗ p.pwmDirectRow = (p.addressingMode == ADDRESSING_MODE_DIRECT);
776 // ...and for the FM6373 family the reverse, because direct row select is
777 // what that family has always done: only an explicit ABC Shift selection
778 // switches it to the token shift register (GH #2955).
779 ✗ p.pwmShiftRow = (p.addressingMode == ADDRESSING_MODE_ABC_SHIFT);
780
781 // OE timing for the FM6373 family, in the reference implementation's units
782 // (DCLK periods) so its published values can be used directly. 0/absent
783 // keeps the default.
784 ✗ if (config.isMember("panelOEClkLength")) {
785 ✗ p.oeClkLength = config["panelOEClkLength"].asInt();
786 }
787 ✗ if (config.isMember("panelOEFirstClkLength")) {
788 ✗ p.oeFirstClkLength = config["panelOEFirstClkLength"].asInt();
789 }
790 ✗ if (config.isMember("panelOERowPeriodUs")) {
791 ✗ p.oeRowPeriodUs = config["panelOERowPeriodUs"].asInt();
792 }
793
794 // FM6373 family register upload grammar. A spacer of 0 is meaningful, so
795 // these carry their historical values as defaults rather than using 0 as
796 // an "unset" marker the way the OE knobs do.
797 ✗ if (config.isMember("panelRegVsyncClocks")) {
798 ✗ p.regVsyncClocks = config["panelRegVsyncClocks"].asInt();
799 }
800 ✗ if (config.isMember("panelRegMidClocks")) {
801 ✗ p.regMidClocks = config["panelRegMidClocks"].asInt();
802 }
803 ✗ if (config.isMember("panelRegPreClocks")) {
804 ✗ p.regPreClocks = config["panelRegPreClocks"].asInt();
805 }
806 ✗ if (config.isMember("panelRegSpacerClocks")) {
807 ✗ p.regSpacerClocks = config["panelRegSpacerClocks"].asInt();
808 }
809 // a burst of 0 clocks would hang the firmware's loop; only the middle
810 // burst may be skipped, and only the spacer may legitimately be 0
811 ✗ if (p.regVsyncClocks < 1 || p.regVsyncClocks > 255) {
812 ✗ p.regVsyncClocks = 3;
813 }
814 ✗ if (p.regMidClocks < 0 || p.regMidClocks > 255) {
815 ✗ p.regMidClocks = 11;
816 }
817 ✗ if (p.regPreClocks < 1 || p.regPreClocks > 255) {
818 ✗ p.regPreClocks = 14;
819 }
820 ✗ if (p.regSpacerClocks < 0 || p.regSpacerClocks > 255) {
821 ✗ p.regSpacerClocks = 8;
822 }
823 // 0 means "unset", and unset must reproduce exactly what this emitted
824 // before these knobs existed - a 600ns pulse and a 2us opener - rather
825 // than the reference's defaults, which convert to slightly different
826 // values and would quietly retime every existing panel of this family.
827 ✗ if (p.oeClkLength < 0 || p.oeClkLength > 255) {
828 ✗ p.oeClkLength = 0;
829 }
830 ✗ if (p.oeFirstClkLength < 0 || p.oeFirstClkLength > 255) {
831 ✗ p.oeFirstClkLength = 0;
832 }
833 ✗ if (p.oeRowPeriodUs < 1 || p.oeRowPeriodUs > 255) {
834 ✗ p.oeRowPeriodUs = 20;
835 }
836
837 // An imported PWM register profile (see www/co-ledPanels.php). The UI
838 // resolves a catalog entry down to its three word lists and stores them
839 // here, so the config is self contained and survives a backup/restore
840 // without the file it came from. Anything malformed is dropped with a
841 // warning rather than half applied - the built-in table is a working
842 // fallback, a truncated upload is not.
843 ✗ if (config.isMember("panelRegisters")) {
844 ✗ const Json::Value& pr = config["panelRegisters"];
845 static const char* KEYS[3] = { "r", "g", "b" };
846 ✗ std::vector<uint16_t> words[3];
847 ✗ bool ok = true;
848 ✗ for (int c = 0; c < 3 && ok; c++) {
849 ✗ if (!pr.isMember(KEYS[c]) || !pr[KEYS[c]].isArray() || pr[KEYS[c]].empty()) {
850 ✗ ok = false;
851 ✗ break;
852 }
853 ✗ for (const auto& w : pr[KEYS[c]]) {
854 // accept both the catalog's bare hex ("025f") and a number
855 ✗ unsigned long v = w.isString() ? strtoul(w.asString().c_str(), nullptr, 16)
856 ✗ : (unsigned long)w.asUInt();
857 ✗ if (v > 0xFFFF) {
858 ✗ ok = false;
859 ✗ break;
860 }
861 ✗ words[c].push_back((uint16_t)v);
862 }
863 }
864 ✗ if (ok && (words[0].size() != words[1].size() || words[0].size() != words[2].size())) {
865 ✗ ok = false;
866 }
867 ✗ if (ok) {
868 ✗ for (int c = 0; c < 3; c++) {
869 ✗ p.regProfile[c] = std::move(words[c]);
870 }
871 ✗ p.regProfileName = pr.isMember("profile") ? pr["profile"].asString() : "imported";
872 } else {
873 ✗ LogWarn(VB_CHANNELOUT, "BBShiftPanel: ignoring malformed panelRegisters profile; using the built-in table\n");
874 }
875 ✗ }
876 ✗ if (p.panelType == PANEL_TYPE_FM6363C) {
877 // the UI moved FM6363C from the addressing dropdown to the panel
878 // type dropdown; internally it stays the PWM addressing mode (old
879 // configs with panelRowAddressType == 51 keep working unchanged)
880 ✗ p.addressingMode = ADDRESSING_MODE_FM6363C;
881 ✗ p.panelType = 0;
882 ✗ } else if (p.panelType == PANEL_TYPE_FM6353) {
883 ✗ p.addressingMode = ADDRESSING_MODE_FM6353C;
884 ✗ p.panelType = 0;
885 ✗ } else if (p.panelType == PANEL_TYPE_FM6373) {
886 ✗ p.addressingMode = ADDRESSING_MODE_FM6373;
887 ✗ p.panelType = 0;
888 ✗ } else if (p.panelType == PANEL_TYPE_DP3364) {
889 ✗ p.addressingMode = ADDRESSING_MODE_DP3364;
890 ✗ p.panelType = 0;
891 ✗ } else if (p.panelType == PANEL_TYPE_ICND1065L) {
892 ✗ p.addressingMode = ADDRESSING_MODE_ICND1065L;
893 ✗ p.panelType = 0;
894 ✗ } else if (p.panelType == PANEL_TYPE_SM16380SH) {
895 ✗ p.addressingMode = ADDRESSING_MODE_SM16380SH;
896 ✗ p.panelType = 0;
897 ✗ } else if (p.panelType == PANEL_TYPE_ICND2153) {
898 ✗ p.addressingMode = ADDRESSING_MODE_ICND2153;
899 ✗ p.panelType = 0;
900 }
901 ✗ bool pwm = p.addressingMode >= ADDRESSING_MODE_FM6353C;
902
903 // A combo cape shares the PRUSS with a string driver on the other PRU:
904 // panels run single-PRU with the smaller split shared-memory ring and
905 // must not touch anything the other driver owns. (The flag is also
906 // honored from the output config for bench testing.)
907 ✗ p.sharedPRUSS = capeConfig["sharedPRUSS"].asBool() || config["sharedPRUSS"].asBool();
908
909 ✗ p.colorDepth = 12;
910 ✗ if (config.isMember("panelColorDepth")) {
911 ✗ p.colorDepth = config["panelColorDepth"].asInt();
912 }
913 ✗ p.outputByRow = false;
914 ✗ p.outputBlankData = false;
915 ✗ if (config.isMember("panelOutputOrder")) {
916 ✗ p.outputByRow = config["panelOutputOrder"].asBool();
917 }
918 ✗ if (config.isMember("panelOutputBlankRow")) {
919 ✗ p.outputBlankData = config["panelOutputBlankRow"].asBool();
920 }
921 ✗ if (p.colorDepth < 0) {
922 ✗ p.colorDepth = -p.colorDepth;
923 ✗ p.outputBlankData = true;
924 }
925 ✗ if (p.colorDepth > 12 || p.colorDepth < 6) {
926 ✗ p.colorDepth = 8;
927 }
928 ✗ if (pwm) {
929 ✗ p.colorDepth = 16;
930 }
931
932 ✗ p.panelInterleave = "";
933 ✗ if (config.isMember("panelInterleave")) {
934 ✗ p.panelInterleave = config["panelInterleave"].asString();
935 }
936 ✗ p.panelScan = config["panelScan"].asInt();
937 ✗ if (p.panelScan == 0) {
938 // default scan is 1/2 the height of the panel
939 ✗ p.panelScan = p.panelHeight / 2;
940 }
941
942 // Full height data layouts. Only meaningful when every row has its own
943 // scan address, because that is what frees the second RGB lane to carry
944 // the other half of the same row instead of the other half of the panel.
945 ✗ p.dataLayout = config["panelDataLayout"].asInt();
946 ✗ if (p.dataLayout) {
947 ✗ if (p.panelScan != p.panelHeight) {
948 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: full height data layout needs scan (%d) to equal panel height (%d); using the standard layout\n",
949 p.panelScan, p.panelHeight);
950 ✗ AddOutputConfigWarning(UNKNOWN_WARNING_ID, "LED panel data layout requires the scan rate to equal the panel height");
951 ✗ p.dataLayout = 0;
952 ✗ } else if (p.panelWidth & 1) {
953 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: full height data layout needs an even panel width (%d); using the standard layout\n",
954 p.panelWidth);
955 ✗ p.dataLayout = 0;
956 ✗ } else if (!p.panelInterleave.empty()) {
957 // Interleave means nothing when every row has its own address.
958 // The UI hides the dropdown for these panels but still saves
959 // whatever it held - "0" (Off), or "8" left over from a smaller
960 // scan - and falling back to the standard layout instead could
961 // never work: it needs a handler, and none exists for scan ==
962 // height.
963 ✗ LogInfo(VB_CHANNELOUT, "BBShiftPanel: ignoring panel interleave '%s' for a full height data layout\n",
964 p.panelInterleave.c_str());
965 ✗ p.panelInterleave = "";
966 }
967 }
968 ✗ return p;
969 ✗ }
970
971 ✗ bool BBShiftPanelManager::parseCapeConfig(const Json::Value& root) {
972 // Mux only the pins the cape actually uses: the named control pins plus
973 // the data pins its outputs reference. On the AM62x every PRU1 pin can
974 // alternatively be muxed to PRU0, so a cape that lists fewer outputs
975 // leaves the unreferenced data pins available for another driver (e.g.
976 // BBShiftString on the other PRU) - the PRU still writes all 8 data
977 // bits of r30, writes to unmuxed bits just never reach a pin. Capes
978 // without a controls section get the full historical pin set.
979 ✗ if (root.isMember("controls") && root["controls"].size()) {
980 ✗ std::set<std::string> pinNames;
981 ✗ for (auto const& name : root["controls"].getMemberNames()) {
982 ✗ pinNames.insert(root["controls"][name]["pin"].asString());
983 ✗ }
984 ✗ for (int i = 0; i < (int)root["outputs"].size(); i++) {
985 ✗ pinNames.insert(root["outputs"][i]["pin"].asString());
986 }
987 ✗ m_configuredPins.assign(pinNames.begin(), pinNames.end());
988 ✗ } else {
989 ✗ m_configuredPins = PRU_PINS;
990 }
991 ✗ for (auto& pinName : m_configuredPins) {
992 ✗ PinCapabilities::getPinByName(pinName).configPin("pru1out", true);
993 }
994 ✗ if (root["controls"].isMember("oe")) {
995 ✗ m_oePin = root["controls"]["oe"]["pin"].asString();
996 }
997 ✗ m_numOutputs = root["outputs"].size();
998 ✗ if (m_numOutputs > MAX_OUTPUTS) {
999 ✗ m_numOutputs = MAX_OUTPUTS;
1000 }
1001 ✗ m_numOutputSlots = 8;
1002 ✗ for (int i = 0; i < m_numOutputs; i++) {
1003 ✗ Json::Value s = root["outputs"][i];
1004 ✗ std::string pinName = s["pin"].asString();
1005 ✗ const BBBPinCapabilities* pin = (const BBBPinCapabilities*)(PinCapabilities::getPinByName(pinName).ptr());
1006 ✗ if (pin == nullptr) {
1007 // a pin name the cape's config gives that this board lacks
1008 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: output %d uses unknown pin '%s'\n", i + 1, pinName.c_str());
1009 ✗ return false;
1010 }
1011 ✗ outputPin[i] = pin->pruPin(1);
1012 ✗ outputBank[i] = 0;
1013 ✗ if (s.isMember("index")) {
1014 ✗ outputBank[i] = s["index"].asInt();
1015 ✗ if (outputBank[i] > 0) {
1016 ✗ m_numOutputSlots = 16;
1017 }
1018 }
1019 ✗ }
1020 ✗ if (root.isMember("singlePRU")) {
1021 ✗ singlePRU = root["singlePRU"].asBool();
1022 }
1023 // singlePRU = true;
1024 ✗ if (!singlePRU) {
1025 // if not using a single PRU, then we need to change the OE pin to the other PRU
1026 ✗ const PinCapabilities& pin = PinCapabilities::getPinByName(m_oePin);
1027 ✗ pin.configPin("pru0out", true);
1028 }
1029 ✗ return true;
1030 }
1031
1032 ✗ bool BBShiftPanelManager::adoptPanelParams(const PanelParams& p) {
1033 ✗ m_panelWidth = p.panelWidth;
1034 ✗ m_panelHeight = p.panelHeight;
1035 ✗ m_panelScan = p.panelScan;
1036 ✗ m_panelInterleave = p.panelInterleave;
1037 ✗ m_addressingMode = p.addressingMode;
1038 ✗ m_panelType = p.panelType;
1039 ✗ m_dataLayout = p.dataLayout;
1040 ✗ m_pwmDirectRow = p.pwmDirectRow;
1041 ✗ m_pwmShiftRow = p.pwmShiftRow;
1042 ✗ m_oeClkLength = p.oeClkLength;
1043 ✗ m_oeFirstClkLength = p.oeFirstClkLength;
1044 ✗ m_oeRowPeriodUs = p.oeRowPeriodUs;
1045 ✗ m_regVsyncClocks = p.regVsyncClocks;
1046 ✗ m_regMidClocks = p.regMidClocks;
1047 ✗ m_regPreClocks = p.regPreClocks;
1048 ✗ m_regSpacerClocks = p.regSpacerClocks;
1049 ✗ buildRegisterProfile(p);
1050 ✗ m_colorDepth = p.colorDepth;
1051 ✗ m_outputByRow = p.outputByRow;
1052 ✗ m_outputBlankData = p.outputBlankData;
1053 ✗ m_sharedPRUSS = p.sharedPRUSS;
1054
1055 ✗ if (m_sharedPRUSS) {
1056 ✗ if (isPWMPanel()) {
1057 ✗ LogErr(VB_CHANNELOUT, "PWM panel types require both PRUs and cannot be used on a shared panels+strings cape\n");
1058 ✗ AddOutputConfigWarning(UNKNOWN_WARNING_ID, "PWM panel types cannot be used on a shared panels+strings cape");
1059 ✗ return false;
1060 }
1061 ✗ singlePRU = true;
1062 }
1063
1064 ✗ if (isPWMPanel()) {
1065 ✗ for (auto& pinName : PRU0_PWM_PINS) {
1066 ✗ const PinCapabilities& pin = PinCapabilities::getPinByName(pinName);
1067 ✗ pin.configPin("pru0out", true);
1068 }
1069 }
1070 ✗ return true;
1071 }
1072
1073 // A second matrix on the cape shares one frame, one stride schedule and one
1074 // set of PRU firmware, so anything that shapes those has to be identical. The
1075 // per-matrix settings (gamma, color order, start corner, brightness, layout)
1076 // are deliberately not in here.
1077 ✗ bool BBShiftPanelManager::checkCompatible(const PanelParams& p) const {
1078 ✗ const char* bad = nullptr;
1079 ✗ if (p.panelWidth != m_panelWidth || p.panelHeight != m_panelHeight) {
1080 ✗ bad = "panel size";
1081 ✗ } else if (p.panelScan != m_panelScan) {
1082 ✗ bad = "panel scan";
1083 ✗ } else if (p.panelInterleave != m_panelInterleave) {
1084 ✗ bad = "panel interleave";
1085 ✗ } else if (p.addressingMode != m_addressingMode || p.panelType != m_panelType) {
1086 ✗ bad = "panel type / row addressing";
1087 ✗ } else if (p.dataLayout != m_dataLayout) {
1088 ✗ bad = "panel data layout";
1089 ✗ } else if (p.colorDepth != m_colorDepth) {
1090 ✗ bad = "color depth";
1091 ✗ } else if (p.outputByRow != m_outputByRow || p.outputBlankData != m_outputBlankData) {
1092 ✗ bad = "output order / blank row";
1093 }
1094 ✗ if (bad) {
1095 ✗ std::string w = std::string("All LED panel matrices on one cape must use the same ") + bad +
1096 ✗ "; this matrix was not started.";
1097 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: %s\n", w.c_str());
1098 ✗ AddOutputConfigWarning(UNKNOWN_WARNING_ID, w);
1099 ✗ return false;
1100 ✗ }
1101 ✗ return true;
1102 }
1103
1104 ✗ bool BBShiftPanelManager::addMember(BBShiftPanelOutput* m, const Json::Value& config, const Json::Value& capeConfig) {
1105 ✗ std::lock_guard<std::mutex> memberLock(m_memberLock);
1106 ✗ PanelParams p = parsePanelParams(config, capeConfig);
1107 ✗ if (m_members.empty()) {
1108 ✗ if (!parseCapeConfig(capeConfig) || !adoptPanelParams(p)) {
1109 ✗ return false;
1110 }
1111 ✗ } else if (!checkCompatible(p)) {
1112 ✗ return false;
1113 }
1114
1115 // Each output is one set of byte lanes in the shared frame, so two
1116 // matrices cannot drive the same one - they would each be writing the
1117 // other's pixels.
1118 ✗ for (int o = 0; o < m_numOutputs; o++) {
1119 ✗ if (!m->usesOutput(o)) {
1120 ✗ continue;
1121 }
1122 ✗ for (auto* other : m_members) {
1123 ✗ if (other->usesOutput(o)) {
1124 ✗ std::string w = "Two LED panel matrices are both configured to use cape output " +
1125 ✗ std::to_string(o + 1) + "; the second matrix was not started.";
1126 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: %s\n", w.c_str());
1127 ✗ AddOutputConfigWarning(UNKNOWN_WARNING_ID, w);
1128 ✗ return false;
1129 ✗ }
1130 }
1131 }
1132
1133 ✗ m_members.push_back(m);
1134 ✗ ++m_openMembers;
1135 ✗ if (!rebuild()) {
1136 ✗ m_members.pop_back();
1137 ✗ --m_openMembers;
1138 ✗ if (m_members.empty()) {
1139 // nothing is left holding the hardware, so do not leave the
1140 // half-built frame buffers allocated
1141 ✗ teardown();
1142 }
1143 ✗ return false;
1144 }
1145 ✗ return true;
1146 ✗ }
1147
1148 // The frame is as wide as the longest chain on the cape and as tall as one
1149 // panel's scan requires; every member maps into that same shape.
1150 ✗ void BBShiftPanelManager::computeGeometry() {
1151 ✗ numRows = 0;
1152 ✗ maxRowLen = 0;
1153 // cleared up front so a member set with no panels leaves a zero sized
1154 // frame rather than the previous rebuild's geometry
1155 ✗ rowLen = 0;
1156
1157 ✗ bool anyPanels = false;
1158 ✗ m_longestChain = 0;
1159 ✗ m_brightness = 1;
1160 ✗ for (auto* m : m_members) {
1161 ✗ if (m->m_panels > 0) {
1162 ✗ anyPanels = true;
1163 }
1164 ✗ if (m->longestChain() > m_longestChain) {
1165 ✗ m_longestChain = m->longestChain();
1166 }
1167 // the OE on-time is shared, so the cape runs at the brightest setting
1168 // any matrix asked for and the others scale their gamma down to it
1169 ✗ if (m->configuredBrightness() > m_brightness) {
1170 ✗ m_brightness = m->configuredBrightness();
1171 }
1172 }
1173 ✗ if (!anyPanels) {
1174 ✗ return;
1175 }
1176
1177 ✗ if (m_dataLayout) {
1178 // Full height: every row has its own scan address, and the two RGB
1179 // lanes split that row down the middle rather than splitting the
1180 // panel top from bottom. Interleave is rejected in parsePanelParams
1181 // for this layout, so there is no handler to consult.
1182 ✗ numRows = m_panelHeight;
1183 ✗ maxRowLen = m_panelWidth / 2;
1184 ✗ rowLen = maxRowLen * m_longestChain;
1185 ✗ return;
1186 }
1187
1188 ✗ PanelInterleaveHandler* handler = PanelInterleaveHandler::createHandler(m_panelInterleave, m_panelWidth, m_panelHeight, m_panelScan);
1189 ✗ if (!handler) {
1190 ✗ LogErr(VB_CHANNELOUT, "Failed to create panel interleave handler\n");
1191 ✗ return;
1192 }
1193 // the mapping only depends on the position within a panel, so one pass
1194 // over a single panel gives the row count and row length for all of them
1195 ✗ for (int y = 0; y < (m_panelHeight / 2); y++) {
1196 ✗ for (int x = 0; x < m_panelWidth; ++x) {
1197 ✗ int yOut = y;
1198 ✗ int xOut = x;
1199 ✗ handler->map(xOut, yOut);
1200 ✗ if (yOut >= (int)numRows) {
1201 ✗ numRows = yOut + 1;
1202 }
1203 ✗ if (xOut >= (int)maxRowLen) {
1204 ✗ maxRowLen = xOut + 1;
1205 }
1206 }
1207 }
1208 ✗ delete handler;
1209 ✗ rowLen = maxRowLen * m_longestChain;
1210 }
1211
1212 // Brings the shared hardware up around the current member set. Called for
1213 // every registration, so adding a matrix with a longer chain re-maps the ones
1214 // already registered onto the new frame width.
1215 ✗ bool BBShiftPanelManager::rebuild() {
1216 ✗ teardownHardware();
1217 // a frame part way through arriving was counted against the old member
1218 // set; the next frame starts over with the new one
1219 ✗ m_preppedThisFrame = 0;
1220 ✗ m_framePrepped = false;
1221 ✗ computeGeometry();
1222 ✗ if (numRows > MAX_SCAN_ROWS) {
1223 // full height panels scan every row (64 for a 64 high panel); the
1224 // per-row tables and the firmware's brightness table stop there
1225 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: %u scan rows, at most %u are supported\n", numRows, MAX_SCAN_ROWS);
1226 ✗ return false;
1227 }
1228
1229 ✗ uint32_t dataSize = intermediateSize();
1230 ✗ if (!dataSize) {
1231 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: no panels configured, nothing to output\n");
1232 ✗ return false;
1233 }
1234 ✗ if (currentChannelData) {
1235 ✗ delete[] currentChannelData;
1236 }
1237 ✗ currentChannelData = new uint16_t[dataSize];
1238 ✗ memset(currentChannelData, 0, dataSize * sizeof(uint16_t));
1239
1240 ✗ for (auto* m : m_members) {
1241 ✗ m->setupGamma();
1242 ✗ if (!m->buildScatterMap()) {
1243 ✗ return false;
1244 }
1245 }
1246
1247 ✗ pru = new BBBPru(1, true, true);
1248 ✗ pruData = (BBShiftPanelData*)pru->data_ram;
1249 ✗ if (!isPWMPanel()) {
1250 ✗ buildStrideSchedule();
1251 }
1252 ✗ if (StartPRU() == 0) {
1253 ✗ return false;
1254 }
1255 ✗ if (isPWMPanel()) {
1256 // A PRU that doesn't take the registers won't take frames either, and
1257 // started anyway the output thread would wait on it every frame.
1258 ✗ if (!setupPWMRegisters()) {
1259 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: the PRU did not take the PWM panel registers; output not started\n");
1260 ✗ AddOutputConfigWarning(UNKNOWN_WARNING_ID, "BBShiftPanel: the PRU did not respond to the PWM panel setup. May require a reboot.");
1261 ✗ return false;
1262 }
1263 } else {
1264 ✗ setupBrightnessValues();
1265 }
1266 ✗ startBackgroundThreads();
1267 ✗ return true;
1268 }
1269
1270 ✗ void BBShiftPanelManager::startBackgroundThreads() {
1271 ✗ if (bgThreadsRunning) {
1272 ✗ return;
1273 }
1274 ✗ bgThreadsRunning = true;
1275 ✗ for (int i = 0; i < std::thread::hardware_concurrency(); i++) {
1276 ✗ std::thread th(&BBShiftPanelManager::runBackgroundTasks, this);
1277 ✗ th.detach();
1278 ✗ }
1279 }
1280
1281 ✗ void BBShiftPanelManager::stopBackgroundThreads() {
1282 ✗ if (!bgThreadsRunning) {
1283 ✗ return;
1284 }
1285 ✗ bgThreadsRunning = false;
1286 ✗ bgTaskCondVar.notify_all();
1287 ✗ while (bgThreadCount > 0) {
1288 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(5));
1289 }
1290 }
1291
1292 ✗ int BBShiftPanelManager::StartPRU() {
1293 ✗ if (!pru) {
1294 ✗ pru = new BBBPru(1, true, true);
1295 ✗ pruData = (BBShiftPanelData*)pru->data_ram;
1296 }
1297 ✗ bool started = true;
1298 ✗ if (isPWMPanel()) {
1299 ✗ pwmPru = new BBBPru(0);
1300 ✗ started &= pwmPru->run("/opt/fpp/src/non-gpl/BBShiftPanel/BBShiftPanel_gclk.out");
1301 ✗ if (m_numOutputSlots == 16) {
1302 ✗ started &= pru->run("/opt/fpp/src/non-gpl/BBShiftPanel/BBShiftPanel_pwm_16.out");
1303 } else {
1304 ✗ started &= pru->run("/opt/fpp/src/non-gpl/BBShiftPanel/BBShiftPanel_pwm.out");
1305 }
1306 } else {
1307 ✗ if (singlePRU) {
1308 // on a shared cape the string driver owns the shared RAM half
1309 // and the other PRU - never clear them from here
1310 ✗ if (m_numOutputSlots == 16) {
1311 ✗ started &= pru->run("/opt/fpp/src/non-gpl/BBShiftPanel/BBShiftPanel_single_16.out", !m_sharedPRUSS);
1312 } else {
1313 ✗ started &= pru->run("/opt/fpp/src/non-gpl/BBShiftPanel/BBShiftPanel_single.out", !m_sharedPRUSS);
1314 }
1315 } else {
1316 // in the two-PRU shift configuration the OE PRU also prefetches
1317 // the ring data and hands it over through the scratchpad, which
1318 // shortens the main PRU's per-block load considerably
1319 ✗ if (m_numOutputSlots == 16) {
1320 ✗ started &= pru->run("/opt/fpp/src/non-gpl/BBShiftPanel/BBShiftPanel_prefetch_16.out");
1321 } else {
1322 ✗ started &= pru->run("/opt/fpp/src/non-gpl/BBShiftPanel/BBShiftPanel_prefetch.out");
1323 }
1324 ✗ pwmPru = new BBBPru(0);
1325 ✗ started &= pwmPru->run("/opt/fpp/src/non-gpl/BBShiftPanel/BBShiftPanel_oe_prefetch.out");
1326 }
1327 }
1328 ✗ if (!started) {
1329 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: Unable to start PRU(s). May require a reboot.\n");
1330 ✗ AddOutputConfigWarning(UNKNOWN_WARNING_ID, "BBShiftPanel: Unable to start PRU(s). May require a reboot.");
1331 ✗ return 0;
1332 }
1333 // The same data RAM word configures either firmware: the shift firmware
1334 // reads its row addressing config right after the ring config handshake,
1335 // the PWM firmware reads the chip family flag at each register upload.
1336 // It must land after run() (the firmware load clears the PRU memories)
1337 // and, for the shift firmware, before the ring attach below.
1338 uint32_t addrCfg;
1339 ✗ if (isPWMPanel()) {
1340 // b0 = chip family, b1 = register slot count, b2 = middle LAT burst
1341 // length (0 = skip it). Only the FM6373 family reads b1/b2.
1342 ✗ if (const PWMChipSeq* seq = activePWMSeq()) {
1343 ✗ addrCfg = 1 | ((uint32_t)seq->slots << 8) | ((seq->midLatch ? 11u : 0u) << 16);
1344 ✗ } else if (m_addressingMode == ADDRESSING_MODE_DP3364) {
1345 // The DP3364S upload is the same shape as the FM6373 one with a
1346 // single register slot and no middle LAT burst: VSYNC (LE 3),
1347 // PRE_ACT (LE 14), then one word latched over its last 5 clocks.
1348 // Sharing that code rather than duplicating it matters - the
1349 // 16 output firmware only just fits in the PRU's 12KB IMEM.
1350 ✗ addrCfg = 1 | (1u << 8) | (0u << 16);
1351 } else {
1352 ✗ addrCfg = 0;
1353 }
1354 } else {
1355 // b2 = row shift chain length: the ABC shift firmware clocks one
1356 // bit per panel row, since panels can daisy chain a row driver per
1357 // band of scan rows
1358 ✗ addrCfg = (uint32_t)(m_addressingMode & 0xFF) | (((uint32_t)numRows) << 8) |
1359 ✗ (((uint32_t)std::min(m_panelHeight, 255)) << 16);
1360 }
1361 ✗ *(volatile uint32_t*)(pru->data_ram + ADDR_CONFIG_OFFSET) = addrCfg;
1362 // The FM6373 family upload grammar, in the word after the chip config:
1363 // the three LAT burst lengths and the LAT-low spacer between them. These
1364 // were fixed at 3/11/14 with a uniform 8 clock spacer, which is not what
1365 // the captures for every chip in the family show - the spacer in
1366 // particular varies per chip and per burst - so a panel that will not take
1367 // the registers has something to sweep. 0 in the middle burst skips it,
1368 // and 0 in the spacer emits no gap at all, which is what the reference
1369 // implementation does.
1370 ✗ if (isPWMPanel()) {
1371 ✗ uint32_t g = ((uint32_t)(m_regVsyncClocks & 0xFF)) |
1372 ✗ ((uint32_t)(m_regMidClocks & 0xFF) << 8) |
1373 ✗ ((uint32_t)(m_regPreClocks & 0xFF) << 16) |
1374 ✗ ((uint32_t)(m_regSpacerClocks & 0xFF) << 24);
1375 ✗ *(volatile uint32_t*)(pru->data_ram + ADDR_CONFIG_OFFSET + 4) = g;
1376 ✗ LogDebug(VB_CHANNELOUT, "BBShiftPanel: register upload grammar vsync=%d mid=%d pre=%d spacer=%d\n",
1377 m_regVsyncClocks, m_regMidClocks, m_regPreClocks, m_regSpacerClocks);
1378 }
1379 ✗ __sync_synchronize();
1380 // Both panel types are fed through a ring in the PRU shared memory (see
1381 // SMEMRing.hp); attach() must happen after run() since the firmware load
1382 // clears the PRU memories (and writes while the PRUSS is powered down
1383 // are silently dropped). A sole owner gets the entire shared RAM; on a
1384 // shared cape the panels are the PRU1 consumer and take the SPLIT1 half
1385 // (BBShiftString's PRU0 program uses SPLIT0, matching its existing
1386 // per-PRU split assignment).
1387 ✗ if (m_sharedPRUSS) {
1388 ✗ m_ring.attach(pru, SMEM_RING_SPLIT1_BASE, SMEM_RING_SPLIT_SIZE, false);
1389 } else {
1390 ✗ m_ring.attach(pru, SMEM_RING_DEFAULT_BASE, SMEM_RING_DEFAULT_SIZE, false);
1391 }
1392 ✗ uint32_t bytesPerPixel = (m_numOutputSlots * 6 * 2) / 16;
1393 ✗ uint32_t strideLen = rowLen * bytesPerPixel;
1394 ✗ uint32_t numStride = m_strideSchedule.empty() ? (numRows * m_colorDepth) : (m_strideSchedule.size() * numRows);
1395 ✗ uint32_t oframeSize = numStride * strideLen;
1396 // PWM panels consume numRows passes of 16 PWM bit planes of numBlocks
1397 // (rowLen / 16) blocks of 16 pixels, which works out to the same full
1398 // frame the pixel mapping produces
1399 ✗ m_frameBytes = oframeSize;
1400 ✗ if (!isPWMPanel()) {
1401 ✗ pruData->numStrides = numStride;
1402 }
1403 // Frames live in normal cached memory now that the pump thread streams
1404 // them to the PRU; the prep threads write cached memory much faster than
1405 // the uncached PRU DDR region this replaced
1406 ✗ uint32_t allocSize = (oframeSize + 4095) & ~4095;
1407 ✗ for (int x = 0; x < NUM_OUTPUT_BUFFERS; x++) {
1408 ✗ outputBuffers[x] = (uint8_t*)aligned_alloc(4096, allocSize);
1409 ✗ memset(outputBuffers[x], 0, allocSize);
1410 }
1411 ✗ m_heapBuffers = true;
1412 ✗ m_frontBuffer = nullptr;
1413 // Reset the handshake counters with it. These outlive the pump thread, so
1414 // on a restart (teardownHardware() then back through here) stale values
1415 // would leave the new thread thinking a frame from the previous
1416 // configuration is still owed, and SendData waiting on a priming that
1417 // already happened.
1418 ✗ m_pumpSeq.store(0, std::memory_order_relaxed);
1419 ✗ m_pumpedSeq = 0;
1420 ✗ m_pumpPrimed.store(0, std::memory_order_relaxed);
1421 ✗ m_pumpRunning = true;
1422 ✗ m_pumpThread = std::thread(&BBShiftPanelManager::runPumpThread, this);
1423 ✗ return 1;
1424 }
1425
1426 ✗ void BBShiftPanelManager::runPumpThread() {
1427 struct sched_param sp;
1428 ✗ sp.sched_priority = 50;
1429 ✗ int err = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp);
1430 ✗ if (err) {
1431 ✗ LogWarn(VB_CHANNELOUT, "BBShiftPanel: could not set pump thread to SCHED_FIFO: %s\n", strerror(err));
1432 }
1433 ✗ if (isPWMPanel()) {
1434 // PWM panels send one frame per DATA command (the panels refresh
1435 // themselves from their internal PWM); stream once per queued
1436 // command so the byte flow stays in step with the commands
1437 ✗ while (m_pumpRunning) {
1438 ✗ if (m_pumpedSeq == m_pumpSeq.load(std::memory_order_acquire)) {
1439 // PrepDataPWM wakes this the moment a frame is published; the
1440 // timeout is only a backstop so shutdown cannot wedge here.
1441 // Polling on a 500us sleep used to leave the PRU shifting
1442 // against an empty ring for most of that wakeup latency.
1443 ✗ std::unique_lock<std::mutex> lk(m_pumpMutex);
1444 ✗ m_pumpCV.wait_for(lk, std::chrono::microseconds(200), [this] {
1445 ✗ return !m_pumpRunning || m_pumpSeq.load(std::memory_order_acquire) != m_pumpedSeq;
1446 });
1447 ✗ continue;
1448 ✗ }
1449 ✗ ++m_pumpedSeq;
1450 ✗ uint8_t* src = m_frontBuffer.load(std::memory_order_acquire);
1451 ✗ if (!src) {
1452 // The sequence can be ahead of the buffer if a reconfigure
1453 // republished it while a frame was in flight; there is nothing
1454 // to stream, and the shift path below makes the same check.
1455 ✗ m_pumpPrimed.store(m_pumpedSeq, std::memory_order_release);
1456 ✗ continue;
1457 }
1458 ✗ uint32_t srcOff = 0;
1459 ✗ bool primed = false;
1460 ✗ while (srcOff < m_frameBytes && m_pumpRunning) {
1461 ✗ uint32_t n = m_ring.write(src + srcOff, std::min(PUMP_BLOCK_SIZE, m_frameBytes - srcOff));
1462 ✗ if (n == 0) {
1463 // the ring holds as much as it can - the PRU may start.
1464 // Back off in small steps: at the drain rate a 150us sleep
1465 // was long enough to open a gap the panel could see.
1466 ✗ if (!primed) {
1467 ✗ primed = true;
1468 ✗ m_pumpPrimed.store(m_pumpedSeq, std::memory_order_release);
1469 }
1470 ✗ struct timespec ts = { 0, 20000 };
1471 ✗ nanosleep(&ts, nullptr);
1472 ✗ continue;
1473 ✗ }
1474 ✗ srcOff += n;
1475 }
1476 // a frame smaller than the ring never fills it
1477 ✗ if (!primed) {
1478 ✗ m_pumpPrimed.store(m_pumpedSeq, std::memory_order_release);
1479 }
1480 }
1481 ✗ return;
1482 }
1483 ✗ while (m_pumpRunning) {
1484 ✗ if (m_pumpPaused.load(std::memory_order_acquire)) {
1485 // Nothing is being output, so there is nothing to hold on the
1486 // panel. Clear the command word and stop: the PRU picks it up at
1487 // the end of the frame it is in and parks in its command-wait
1488 // loop, which holds the display off, and the OE PRU parks the
1489 // same way once the brightness handshake stops. SendData writes
1490 // the command back on the first frame after StartingOutput.
1491 //
1492 // This is only reached at a frame boundary, and the ring is much
1493 // smaller than one frame, so the PRU has at most the tail of the
1494 // frame just written left to consume. It ends up exactly where
1495 // the pump stopped and the two stay aligned in the byte stream
1496 // across the pause.
1497 ✗ if (!m_pumpParked.load(std::memory_order_relaxed)) {
1498 ✗ pruData->pixelsPerStride = 0;
1499 ✗ __sync_synchronize();
1500 ✗ m_pumpParked.store(true, std::memory_order_release);
1501 }
1502 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(10));
1503 ✗ continue;
1504 }
1505 ✗ m_pumpParked.store(false, std::memory_order_relaxed);
1506 // frames stream back to back; a new frame from SendData is picked up
1507 // at the frame boundary so the PRU always sees whole frames
1508 ✗ uint8_t* src = m_frontBuffer.load(std::memory_order_acquire);
1509 ✗ if (!src) {
1510 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
1511 ✗ continue;
1512 }
1513 ✗ uint32_t srcOff = 0;
1514 ✗ while (srcOff < m_frameBytes && m_pumpRunning) {
1515 ✗ uint32_t n = m_ring.write(src + srcOff, std::min(PUMP_BLOCK_SIZE, m_frameBytes - srcOff));
1516 ✗ if (n == 0) {
1517 // ring full; a full ring lasts ~480us at the max drain rate
1518 // so a short sleep cannot underrun the PRU
1519 ✗ struct timespec ts = { 0, 150000 };
1520 ✗ nanosleep(&ts, nullptr);
1521 ✗ continue;
1522 ✗ }
1523 ✗ srcOff += n;
1524 }
1525 }
1526 }
1527 ✗ void BBShiftPanelManager::StopPRU(bool wait) {
1528 // A halted core cannot see the stop command, and a parked one is already
1529 // sitting on the check, so let them run first
1530 ✗ UnparkPRUs();
1531 // Send the stop command
1532 ✗ if (pru) {
1533 ✗ pruData->command = PWM_COMMAND_HALT;
1534 ✗ pruData->result = PWM_COMMAND_HALT;
1535 }
1536 ✗ __asm__ __volatile__("" ::
1537 : "memory");
1538
1539 ✗ if (pru) {
1540 ✗ int cnt = 0;
1541 // the pump keeps feeding the ring here so the PRU can reach the end
1542 // of the frame, which is where the halt command is checked
1543 ✗ while (wait && cnt < 25 && pruData->result == PWM_COMMAND_HALT) {
1544 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
1545 ✗ cnt++;
1546 ✗ __asm__ __volatile__("" ::
1547 : "memory");
1548 }
1549 }
1550 // stop the pump before the PRU memory mappings go away
1551 ✗ m_pumpRunning = false;
1552 ✗ if (m_pumpThread.joinable()) {
1553 ✗ m_pumpThread.join();
1554 }
1555 ✗ if (pru) {
1556 ✗ pru->stop();
1557 ✗ delete pru;
1558 ✗ pru = nullptr;
1559 // pruData points into the mapping that just went away. Code that
1560 // finds it set assumes the PRU is up (startingOutput() sends the
1561 // panel init through pru on that basis), and after a failed rebuild
1562 // the surviving matrices keep calling in.
1563 ✗ pruData = nullptr;
1564
1565 ✗ if (pwmPru) {
1566 ✗ pwmPru->stop();
1567 ✗ delete pwmPru;
1568 ✗ pwmPru = nullptr;
1569 }
1570 }
1571 ✗ }
1572
1573 ✗ int BBShiftPanelOutput::Close(void) {
1574 ✗ LogDebug(VB_CHANNELOUT, "BBShiftPanelOutput::Close()\n");
1575 ✗ if (!m_autoCreatedModelName.empty()) {
1576 ✗ PixelOverlayManager::INSTANCE.removeAutoOverlayModel(m_autoCreatedModelName);
1577 ✗ m_autoCreatedModelName.clear();
1578 }
1579 ✗ releaseToManager();
1580 ✗ return ChannelOutput::Close();
1581 }
1582
1583 28 void BBShiftPanelOutput::releaseToManager() {
1584
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
28 if (m_registered && !m_closed) {
1585 ✗ m_closed = true;
1586 ✗ BBShiftPanelManager::INSTANCE.closeMember();
1587 }
1588 28 }
1589
1590 ✗ void BBShiftPanelManager::closeMember() {
1591 ✗ if (m_openMembers > 0 && --m_openMembers) {
1592 // the pins stay muxed while another matrix on the cape still needs them
1593 ✗ return;
1594 }
1595 ✗ if (!m_refreshWarning.empty()) {
1596 ✗ WarningHolder::RemoveWarning(m_refreshWarning);
1597 ✗ m_refreshWarning.clear();
1598 }
1599 // release only the pins this cape muxed; other drivers (a future
1600 // panels + strings combo cape) may own the rest
1601 ✗ for (auto& pinName : m_configuredPins) {
1602 ✗ const PinCapabilities& pin = PinCapabilities::getPinByName(pinName);
1603 ✗ pin.releasePin();
1604 }
1605 ✗ if (!singlePRU) {
1606 // if not using a single PRU, then we need to change the OE pin to the other PRU
1607 ✗ const PinCapabilities& pin = PinCapabilities::getPinByName(m_oePin);
1608 ✗ pin.releasePin();
1609 }
1610 ✗ if (isPWMPanel()) {
1611 ✗ for (auto& pinName : PRU0_PWM_PINS) {
1612 ✗ const PinCapabilities& pin = PinCapabilities::getPinByName(pinName);
1613 ✗ pin.releasePin();
1614 }
1615 }
1616 ✗ m_configuredPins.clear();
1617 }
1618
1619 ✗ void BBShiftPanelManager::runBackgroundTasks() {
1620 ✗ ++bgThreadCount;
1621 ✗ while (bgThreadsRunning) {
1622 ✗ std::function<void()> task;
1623 {
1624 ✗ std::unique_lock<std::mutex> lock(bgTaskMutex);
1625 ✗ bgTaskCondVar.wait_for(lock, std::chrono::milliseconds(100), [&]() { return !bgTasks.empty(); });
1626 ✗ if (!bgTasks.empty()) {
1627 ✗ task = bgTasks.front();
1628 ✗ bgTasks.pop();
1629 }
1630 ✗ }
1631 ✗ if (task) {
1632 ✗ task();
1633 }
1634 ✗ }
1635 ✗ --bgThreadCount;
1636 ✗ }
1637
1638 ✗ void BBShiftPanelManager::processTasks(std::atomic<int>& counter) {
1639 // This must always drain to zero, even during shutdown: the queued tasks
1640 // reference the calling frame's stack (counter, results) and this object,
1641 // so returning while any are queued or running leaves them with dangling
1642 // references. The pool threads exit without draining the queue, but this
1643 // loop executes leftover tasks itself so it cannot deadlock.
1644 ✗ std::unique_lock<std::mutex> lock(bgTaskMutex);
1645 ✗ while (counter > 0) {
1646 ✗ std::function<void()> task;
1647 ✗ if (!bgTasks.empty()) {
1648 ✗ task = bgTasks.front();
1649 ✗ bgTasks.pop();
1650 }
1651 ✗ lock.unlock();
1652 ✗ if (task) {
1653 ✗ task();
1654 } else {
1655 ✗ std::this_thread::sleep_for(std::chrono::microseconds(100));
1656 }
1657 ✗ lock.lock();
1658 ✗ }
1659 ✗ }
1660
1661 ✗ void BBShiftPanelOutput::PrepData(unsigned char* channelData) {
1662 ✗ LogExcess(VB_CHANNELOUT, "BBShiftPanelOutput::PrepData(%p)\n", channelData);
1663 ✗ if (!m_registered) {
1664 ✗ return;
1665 }
1666 ✗ ++m_inFlight;
1667 ✗ BBShiftPanelManager::INSTANCE.prepMember(this, channelData);
1668 ✗ --m_inFlight;
1669 }
1670
1671 // Scatters this matrix's channels into its own byte lanes of the shared 16 bit
1672 // intermediate. Members never touch each other's lanes, so this is safe to run
1673 // for each matrix in turn with no coordination beyond the frame boundary below.
1674 ✗ void BBShiftPanelOutput::scatterFrame(unsigned char* channelData) {
1675 ✗ auto& mgr = BBShiftPanelManager::INSTANCE;
1676 ✗ uint16_t* dest = mgr.currentChannelData;
1677 ✗ if (!dest) {
1678 ✗ return;
1679 }
1680 ✗ m_matrix->OverlaySubMatrices(channelData);
1681 ✗ channelData += m_startChannel;
1682
1683 ✗ std::unique_lock<std::mutex> lock(mgr.bgTaskMutex);
1684 ✗ size_t total = m_scatterOffsets.size();
1685 ✗ size_t start = 0;
1686 ✗ std::atomic<int> counter(0);
1687 ✗ while (start < total) {
1688 // each chunk writes a contiguous region of the output since the map
1689 // is sorted by destination
1690 ✗ size_t end = std::min(start + 64 * 1024, total);
1691 ✗ ++counter;
1692 ✗ mgr.bgTasks.push([this, channelData, start, end, dest, &counter]() {
1693 ✗ const uint32_t* offs = m_scatterOffsets.data();
1694 ✗ const uint32_t* srcs = m_scatterSrc.data();
1695 ✗ for (size_t x = start; x < end; x++) {
1696 ✗ dest[offs[x]] = gammaCurve[channelData[srcs[x]]];
1697 }
1698 ✗ --counter;
1699 ✗ });
1700 ✗ start = end;
1701 }
1702 ✗ lock.unlock();
1703 ✗ mgr.bgTaskCondVar.notify_all();
1704 ✗ mgr.processTasks(counter);
1705 ✗ }
1706
1707 // One frame's worth of work for the whole cape. Every member scatters into
1708 // the shared intermediate first; the bit-plane pass is expensive and produces
1709 // a single frame for all of them, so it runs once, when the last member has
1710 // arrived. Doing it here rather than in SendData keeps it out of the way of
1711 // the other outputs' wire sends.
1712 ✗ void BBShiftPanelManager::prepMember(BBShiftPanelOutput* m, unsigned char* channelData) {
1713 ✗ std::lock_guard<std::mutex> memberLock(m_memberLock);
1714 ✗ if (!m->m_stopping) {
1715 ✗ m->scatterFrame(channelData);
1716 }
1717 // A stopping member contributes nothing but must still be counted, or the
1718 // frame boundary would never be reached again and output would stall.
1719 ✗ if (++m_preppedThisFrame < (int)m_members.size()) {
1720 ✗ return;
1721 }
1722 ✗ m_preppedThisFrame = 0;
1723 ✗ if (!bgThreadsRunning) {
1724 ✗ return;
1725 }
1726 ✗ if (isPWMPanel()) {
1727 ✗ if (!PrepDataPWM()) {
1728 ✗ return; // dropped: the PRU didn't take the previous command
1729 }
1730 } else {
1731 ✗ PrepDataShift();
1732 }
1733 ✗ m_framePrepped = true;
1734 ✗ }
1735
1736 ✗ bool BBShiftPanelManager::PrepDataPWM() {
1737 ✗ uint8_t* buf = outputBuffers[currOutputBuffer];
1738
1739 ✗ std::unique_lock<std::mutex> lock(bgTaskMutex);
1740 ✗ std::atomic<int> counter(0);
1741 ✗ for (int curRow = 0; curRow < numRows; curRow++) {
1742 // Map the pixels for this row
1743 ✗ ++counter;
1744 ✗ bgTasks.push([this, curRow, buf, &counter]() {
1745 ✗ uint32_t start = curRow * rowLen;
1746 ✗ uint32_t end = start + rowLen;
1747
1748 ✗ if (m_numOutputSlots == 16) {
1749 ✗ ispc::MapPixelsForPWM16(currentChannelData, start, end, (uint16_t*)buf);
1750 } else {
1751 ✗ ispc::MapPixelsForPWM(currentChannelData, start, end, (uint16_t*)buf);
1752 }
1753 ✗ --counter;
1754 ✗ });
1755 }
1756 ✗ lock.unlock();
1757 ✗ bgTaskCondVar.notify_all();
1758 ✗ processTasks(counter);
1759
1760 /*
1761 for (int x = 0; x < 48; x++) {
1762 printf("%04x ", currentChannelData[x]);
1763 }
1764 printf("\n");
1765 for (int x = 0; x < 96; x += 6) {
1766 printf("%02x %02x %02x %02x %02x %02x\n", buf[x], buf[x + 1], buf[x + 2], buf[x + 3], buf[x + 4], buf[x + 5]);
1767 }
1768 printf("\n");
1769 for (int x = 48; x < 96; x++) {
1770 printf("%04x ", currentChannelData[x]);
1771 }
1772 printf("\n");
1773 for (int x = 96; x < (96 * 2); x += 6) {
1774 printf("%02x %02x %02x %02x %02x %02x\n", buf[x], buf[x + 1], buf[x + 2], buf[x + 3], buf[x + 4], buf[x + 5]);
1775 }
1776 printf("\n");
1777 */
1778
1779 // wait for the PRU to take the previous command before queueing the next
1780 ✗ if (!waitForPWMCommandTaken()) {
1781 ✗ return false;
1782 }
1783
1784 // hand the frame to the pump thread
1785 ✗ m_frontBuffer.store(buf, std::memory_order_release);
1786 ✗ uint32_t seq = m_pumpSeq.fetch_add(1, std::memory_order_release) + 1;
1787 ✗ m_pumpCV.notify_one();
1788 // Hold the DATA command until the ring is primed. Starting the PRU
1789 // against an empty ring left it in RINGWAIT for the pump's wakeup
1790 // latency, and an FM6363C reads that gap as its row scan running away
1791 // from the data. Filling the ring takes ~65us; the loop bound is only a
1792 // backstop so a stopped pump cannot hang the output thread.
1793 ✗ for (int i = 0; i < 4000 && m_pumpRunning && bgThreadsRunning &&
1794 ✗ m_pumpPrimed.load(std::memory_order_acquire) != seq;
1795 ++i) {
1796 ✗ std::this_thread::yield();
1797 }
1798
1799 ✗ pruData->numBlocks = rowLen / 16;
1800 ✗ pruData->numRows = numRows;
1801 ✗ pruData->cmd = PWM_COMMAND_DATA;
1802 ✗ __asm__ __volatile__("" ::
1803 : "memory");
1804 ✗ return true;
1805 ✗ }
1806
1807 // The PRU clears the command word when it takes a command. These waits run on
1808 // the output thread holding outputThreadLock and m_memberLock, so a PRU that
1809 // stopped answering (a wedged or crashed firmware) must not hold them forever:
1810 // that stopped all output, wedged the next config reload and kept fppd from
1811 // shutting down. Give it a few frame periods, then drop the frame. Once it
1812 // has missed one, later frames don't wait at all until it answers again.
1813 ✗ bool BBShiftPanelManager::waitForPWMCommandTaken() {
1814 ✗ int limit = m_pwmPruStalled ? 0 : 1000; // 100ms
1815 ✗ for (int cnt = 0; pruData->command && bgThreadsRunning && cnt < limit; cnt++) {
1816 ✗ std::this_thread::sleep_for(std::chrono::microseconds(100));
1817 ✗ __asm__ __volatile__("" ::
1818 : "memory");
1819 }
1820 ✗ if (!pruData->command) {
1821 ✗ if (m_pwmPruStalled) {
1822 ✗ LogInfo(VB_CHANNELOUT, "BBShiftPanel: the PRU is taking PWM panel frames again\n");
1823 ✗ m_pwmPruStalled = false;
1824 ✗ if (!m_pwmStallWarning.empty()) {
1825 ✗ WarningHolder::RemoveWarning(m_pwmStallWarning);
1826 ✗ m_pwmStallWarning.clear();
1827 }
1828 }
1829 ✗ return bgThreadsRunning;
1830 }
1831 ✗ if (bgThreadsRunning && !m_pwmPruStalled) {
1832 ✗ m_pwmPruStalled = true;
1833 ✗ LogErr(VB_CHANNELOUT, "BBShiftPanel: the PRU stopped taking PWM panel frames; dropping frames until it answers\n");
1834 ✗ m_pwmStallWarning = "BBShiftPanel: the PRU stopped responding; LED panel frames are being dropped. May require a reboot.";
1835 ✗ WarningHolder::AddWarning(m_pwmStallWarning);
1836 }
1837 ✗ return false;
1838 }
1839
1840 ✗ void BBShiftPanelManager::PrepDataShift() {
1841 // rebuild() refuses a geometry with more rows than this
1842 std::array<std::array<uint16_t*, 16>, MAX_SCAN_ROWS> results;
1843
1844 ✗ uint32_t bytesPerPixel = (m_numOutputSlots * 6 * 2) / 16;
1845 ✗ uint32_t strideLen = rowLen * bytesPerPixel;
1846 ✗ uint8_t* base = outputBuffers[currOutputBuffer];
1847 ✗ int numSlots = m_strideSchedule.size();
1848 ✗ for (int s = 0; s < numSlots; s++) {
1849 ✗ if (!m_strideSchedule[s].primary) {
1850 ✗ continue;
1851 }
1852 ✗ int b = m_strideSchedule[s].bit;
1853 ✗ for (int r = 0; r < numRows; r++) {
1854 ✗ uint32_t idx = m_outputByRow ? (r * numSlots + s) : (s * numRows + r);
1855 ✗ results[r][b] = (uint16_t*)(base + idx * strideLen);
1856 }
1857 }
1858
1859 /*
1860 int len = rowLen * numRows * 6 * 8
1861 uint16_t *d = data;
1862 for (int x = 0; x < len; x += 8) {
1863 for (int y = 0; y < 8; y++) {
1864 uint16_t d2 = *d;
1865 uint16_t mask = 0x1;
1866 uint8_t bit = 0x1 << y;
1867 for (int pos = 0; pos < bits; pos++) {
1868 if (d2 & mask) {
1869 results[pos][x] |= bit;
1870 }
1871 mask <<= 1;
1872 }
1873 ++d;
1874 }
1875 }
1876 */
1877 // Use ISPC generated code for the above. It's about 9x faster
1878 ✗ std::unique_lock<std::mutex> lock(bgTaskMutex);
1879 ✗ std::atomic<int> counter(0);
1880 ✗ for (int curRow = 0; curRow < numRows; curRow++) {
1881 // Map the pixels for this row
1882 ✗ ++counter;
1883 ✗ bgTasks.push([this, curRow, strideLen, base, &results, &counter]() {
1884 ✗ uint32_t start = curRow * rowLen * 6 * m_numOutputSlots;
1885 ✗ uint32_t end = start + (rowLen * 6 * m_numOutputSlots);
1886 ✗ ispc::MapPixelsByDepth16(currentChannelData, start, end, m_colorDepth,
1887 ✗ results[curRow][0], results[curRow][1],
1888 ✗ results[curRow][2], results[curRow][3],
1889 ✗ results[curRow][4], results[curRow][5],
1890 ✗ results[curRow][6], results[curRow][7],
1891 ✗ results[curRow][8], results[curRow][9],
1892 ✗ results[curRow][10], results[curRow][11],
1893 ✗ results[curRow][12], results[curRow][13],
1894 ✗ results[curRow][14], results[curRow][15]);
1895 // fill in the duplicated strides for bits that display as multiple pulses
1896 ✗ for (auto& dup : m_dupCopies[curRow]) {
1897 ✗ memcpy(base + dup.first, base + dup.second, strideLen);
1898 }
1899 ✗ --counter;
1900 ✗ });
1901 }
1902 ✗ lock.unlock();
1903 ✗ bgTaskCondVar.notify_all();
1904 ✗ processTasks(counter);
1905 ✗ }
1906
1907 ✗ int BBShiftPanelOutput::SendData(unsigned char* channelData) {
1908 ✗ LogExcess(VB_CHANNELOUT, "BBShiftPanelOutput::SendData(%p)\n", channelData);
1909 ✗ if (!m_registered || m_stopping) {
1910 ✗ return m_channelCount;
1911 }
1912 ✗ ++m_inFlight;
1913 ✗ BBShiftPanelManager::INSTANCE.publishFrame();
1914 ✗ --m_inFlight;
1915 ✗ return m_channelCount;
1916 }
1917
1918 // Hands the frame the last PrepData built to the pump. Every member calls
1919 // this, but the frame is shared, so only the first call after a prep does
1920 // anything - the rest are no-ops. This is deliberately cheap: a pointer
1921 // store and a couple of PRU words, nothing that would hold up the other
1922 // outputs waiting to send.
1923 ✗ void BBShiftPanelManager::publishFrame() {
1924 ✗ std::lock_guard<std::mutex> memberLock(m_memberLock);
1925 ✗ if (!m_framePrepped) {
1926 ✗ return;
1927 }
1928 ✗ m_framePrepped = false;
1929
1930 ✗ if (!isPWMPanel()) {
1931 // hand the just-prepared frame to the pump thread; it picks it up at
1932 // its next frame boundary so the PRU always sees whole frames
1933 ✗ m_frontBuffer.store(outputBuffers[currOutputBuffer], std::memory_order_release);
1934 ✗ pruData->numStrides = m_strideSchedule.size() * numRows;
1935 ✗ pruData->pixelsPerStride = rowLen;
1936 } else {
1937 ✗ if (!waitForPWMCommandTaken()) {
1938 ✗ return; // dropped: the PRU didn't take the frame's DATA command
1939 }
1940
1941 // Send the command to setup the registers
1942 ✗ pruData->numBlocks = rowLen / 16;
1943 ✗ pruData->numRows = numRows;
1944 ✗ if (const PWMChipSeq* seq = activePWMSeq()) {
1945 // rotate the config sequence one word per frame as a continuous
1946 // refresh; the vsync is part of the FM6373 register upload so
1947 // PWM_COMMAND_SYNC is not set for this family
1948 ✗ writeFM6373SeqWord(m_pwmSeqIdx);
1949 ✗ m_pwmSeqIdx = (m_pwmSeqIdx + 1) % seq->len;
1950 ✗ pruData->cmd = PWM_COMMAND_REGISTERS | PWM_COMMAND_STARTGCLK;
1951 ✗ } else if (m_addressingMode == ADDRESSING_MODE_DP3364) {
1952 // same one-register-per-frame rotation; the datasheet frames it
1953 // as the intended way to refresh all 15 registers without paying
1954 // for a full upload every frame. The VSYNC is the first LE pulse
1955 // of the DP3364S register upload, so PWM_COMMAND_SYNC is not set.
1956 ✗ writeDP3364SeqWord(m_pwmSeqIdx);
1957 ✗ m_pwmSeqIdx = (m_pwmSeqIdx + 1) % DP3364_SEQ_LEN;
1958 ✗ pruData->cmd = PWM_COMMAND_REGISTERS | PWM_COMMAND_STARTGCLK;
1959 } else {
1960 ✗ pruData->cmd = PWM_COMMAND_REGISTERS | PWM_COMMAND_SYNC | PWM_COMMAND_STARTGCLK;
1961 }
1962 }
1963
1964 ✗ __asm__ __volatile__("" ::
1965 : "memory");
1966 ✗ currOutputBuffer = (currOutputBuffer + 1) % NUM_OUTPUT_BUFFERS;
1967 ✗ while (outputBuffers[currOutputBuffer] == nullptr) {
1968 ✗ currOutputBuffer = (currOutputBuffer + 1) % NUM_OUTPUT_BUFFERS;
1969 }
1970 ✗ }
1971 ✗ inline int mapRow(int row, int mode) {
1972 ✗ if (mode == ADDRESSING_MODE_DIRECT) {
1973 ✗ switch (row) {
1974 ✗ case 0:
1975 ✗ return 0x0E;
1976 ✗ case 1:
1977 ✗ return 0x0D;
1978 ✗ case 2:
1979 ✗ return 0x0B;
1980 ✗ case 3:
1981 ✗ return 0x07;
1982 }
1983 }
1984 ✗ return row;
1985 }
1986
1987 ✗ static int outputRegData(int curidx, uint8_t* odata, uint16_t r, uint16_t g, uint16_t b, int numOutputs = 8) {
1988 // int sidx = curidx;
1989 ✗ int bytesPerClock = numOutputs == 16 ? 12 : 6;
1990 ✗ for (int x = 0; x < 16; x++) {
1991 ✗ odata[curidx] = r & 0x8000 ? 0xFF : 0x00;
1992 ✗ curidx++;
1993 ✗ odata[curidx] = g & 0x8000 ? 0xFF : 0x00;
1994 ✗ curidx++;
1995 ✗ odata[curidx] = b & 0x8000 ? 0xFF : 0x00;
1996 ✗ curidx++;
1997 ✗ odata[curidx] = r & 0x8000 ? 0xFF : 0x00;
1998 ✗ curidx++;
1999 ✗ odata[curidx] = g & 0x8000 ? 0xFF : 0x00;
2000 ✗ curidx++;
2001 ✗ odata[curidx] = b & 0x8000 ? 0xFF : 0x00;
2002 ✗ curidx++;
2003 ✗ if (numOutputs == 16) {
2004 // Duplicate for outputs 8-15
2005 ✗ odata[curidx] = r & 0x8000 ? 0xFF : 0x00;
2006 ✗ curidx++;
2007 ✗ odata[curidx] = g & 0x8000 ? 0xFF : 0x00;
2008 ✗ curidx++;
2009 ✗ odata[curidx] = b & 0x8000 ? 0xFF : 0x00;
2010 ✗ curidx++;
2011 ✗ odata[curidx] = r & 0x8000 ? 0xFF : 0x00;
2012 ✗ curidx++;
2013 ✗ odata[curidx] = g & 0x8000 ? 0xFF : 0x00;
2014 ✗ curidx++;
2015 ✗ odata[curidx] = b & 0x8000 ? 0xFF : 0x00;
2016 ✗ curidx++;
2017 }
2018 ✗ r <<= 1;
2019 ✗ g <<= 1;
2020 ✗ b <<= 1;
2021 }
2022 /*
2023 if (sidx == 0) {
2024 printf("%04X %04X %04X\n", r, g, b);
2025 for (int x = 0; x < 16; x++) {
2026 printf(" %02x %02x %02x %02x %02x %02x\n",
2027 odata[sidx], odata[sidx + 1], odata[sidx + 2],
2028 odata[sidx + 3], odata[sidx + 4], odata[sidx + 5]);
2029 sidx += 6;
2030 }
2031 }
2032 */
2033 ✗ return curidx;
2034 }
2035
2036 ✗ bool BBShiftPanelManager::setupPWMRegisters() {
2037 // This runs from Init(), on the main loop while outputs load. The waits
2038 // for the PRU to take a command had no limit, so a PRU that stopped
2039 // answering (firmware that failed to load, or wedged) hung fppd at
2040 // startup. Same budget as the panel init registers above.
2041 ✗ auto pruIdle = [this]() {
2042 ✗ for (int cnt = 0; pruData->command && cnt < 2500; cnt++) {
2043 ✗ std::this_thread::sleep_for(std::chrono::microseconds(100));
2044 ✗ __asm__ __volatile__("" ::
2045 : "memory");
2046 }
2047 ✗ if (pruData->command) {
2048 ✗ LogWarn(VB_CHANNELOUT, "BBShiftPanel: PWM panel registers were not sent (PRU did not respond)\n");
2049 ✗ return false;
2050 }
2051 ✗ return true;
2052 ✗ };
2053 /*
2054 // FM6363 from colorlight + logic analyzer
2055 // Confirmed via "advanced" tab in LEDVision
2056 Reg 1: 0x0970: b0000100101110000
2057 Reg 2: 0xFF9B R: b1111111110011011 (default values)
2058 0xF39B G: b1111001110011011
2059 0xDF9B B: b1101111110011011
2060 Reg 2: 0xFE01 R: b1111110000000001 (with current stripped off)
2061 0xF201 G: b1111000000000001
2062 0xDE01 B: b1101110000000001
2063 Reg 3: 0x4007 b0100000000000111
2064 Reg 4: 0x0040 b0000000001000000
2065 Reg 5: 0x0000 b0000000000000000
2066 */
2067 static uint16_t conf_6363[] = {
2068 // R/G/B triplets
2069 0x0070, 0x0070, 0x0070,
2070 0xFC01, 0xF001, 0xDC01,
2071 0x4007, 0x4007, 0x4007,
2072 0x0040, 0x0040, 0x0040,
2073 0x0000, 0x0000, 0x0000
2074 };
2075
2076 // create the "data" array of for all outputs of r/g/b triplets for the registers
2077 // 16 clocks * bytesPerClock * 5 registers
2078 ✗ int bytesPerClock = m_numOutputSlots == 16 ? 12 : 6;
2079 uint8_t odata[192 * 6];
2080
2081 ✗ if (m_addressingMode == ADDRESSING_MODE_DP3364) {
2082 // DP3364S: one config register per frame, so only the single rotating
2083 // word slot is used (the VSYNC and PRE_ACT that precede it are bare LE
2084 // pulses with the data lines "not care" - see OUTPUT_REGISTERS_DP3364
2085 // in the asm). Clock the whole sequence through before the first
2086 // frame, then SendData keeps rotating it as the continuous refresh the
2087 // datasheet asks for.
2088 ✗ setupGCLKConfig();
2089 ✗ pruData->numBlocks = rowLen / 16;
2090 ✗ pruData->numRows = numRows;
2091 ✗ m_pwmSeqIdx = 0;
2092 ✗ for (int i = 0; i < DP3364_SEQ_LEN; i++) {
2093 ✗ if (!pruIdle()) {
2094 ✗ return false;
2095 }
2096 ✗ writeDP3364SeqWord(i);
2097 ✗ pruData->cmd = PWM_COMMAND_REGISTERS;
2098 ✗ __asm__ __volatile__("" ::
2099 : "memory");
2100 // the command word clears when the PRU *starts* the upload, so
2101 // give it time to finish before rewriting the rotating slot
2102 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
2103 }
2104 // and that it took the last one
2105 ✗ return pruIdle();
2106 }
2107
2108 ✗ if (const PWMChipSeq* seq = activePWMSeq()) {
2109 // FM6373 family: the per-frame words are the 0x00AA/0x01AA
2110 // write-enable pair, one word of the config register sequence, an
2111 // optional chip specific word, and the 0x0055/0x0155 commit pair (see
2112 // OUTPUT_REGISTERS_FM6373 in the asm). Load the fixed slots once,
2113 // then clock the entire sequence through the rotating slot so the
2114 // panel is fully configured before the first frame; SendData keeps
2115 // rotating it afterwards as a continuous refresh (DMD_STM32 and
2116 // kingdo9 both do the same).
2117 ✗ int idx = outputRegData(0, odata, 0x00AA, 0x00AA, 0x00AA, m_numOutputSlots);
2118 ✗ idx = outputRegData(idx, odata, 0x01AA, 0x01AA, 0x01AA, m_numOutputSlots);
2119 ✗ idx = outputRegData(idx, odata, seq->r[0], seq->g[0], seq->b[0], m_numOutputSlots);
2120 ✗ if (seq->slots == 6) {
2121 ✗ idx = outputRegData(idx, odata, seq->extraWord, seq->extraWord, seq->extraWord, m_numOutputSlots);
2122 }
2123 ✗ idx = outputRegData(idx, odata, 0x0055, 0x0055, 0x0055, m_numOutputSlots);
2124 ✗ idx = outputRegData(idx, odata, 0x0155, 0x0155, 0x0155, m_numOutputSlots);
2125 ✗ pru->memcpyToPRU((uint8_t*)&pruData->registers[0], &odata[0], idx);
2126
2127 // An imported profile was picked for this panel deliberately, so it
2128 // is the authority on its own scan rate and the built-in capture's
2129 // rate says nothing about it.
2130 ✗ bool haveScan = m_haveProfile || ((int)numRows == seq->defaultScan);
2131 ✗ for (int v = 0; v < seq->variantCount && !haveScan; v++) {
2132 ✗ haveScan = (seq->variants[v].scan == (int)numRows);
2133 }
2134 ✗ if (!haveScan) {
2135 // These tables are per-capture, not per-chip: on this family the
2136 // current, subfield and tail registers move with the scan rate as
2137 // well as the scan count itself, so an unmatched rate is a
2138 // starting point rather than a correct configuration.
2139 ✗ LogWarn(VB_CHANNELOUT, "BBShiftPanel: no register table captured at 1/%u scan for this panel type; using the 1/%d table with only the scan count adjusted\n",
2140 numRows, seq->defaultScan);
2141 }
2142
2143 ✗ setupGCLKConfig();
2144 ✗ pruData->numBlocks = rowLen / 16;
2145 ✗ pruData->numRows = numRows;
2146 ✗ m_pwmSeqIdx = 0;
2147 ✗ for (int i = 0; i < seq->len; i++) {
2148 ✗ if (!pruIdle()) {
2149 ✗ return false;
2150 }
2151 ✗ writeFM6373SeqWord(i);
2152 ✗ pruData->cmd = PWM_COMMAND_REGISTERS;
2153 ✗ __asm__ __volatile__("" ::
2154 : "memory");
2155 // the command word clears when the PRU *starts* the upload, so
2156 // give it time to finish before rewriting the rotating slot
2157 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
2158 }
2159 // and that it took the last one
2160 ✗ return pruIdle();
2161 }
2162
2163 // register 1 contains the number of scan lines (rows)
2164 ✗ uint16_t rn = ((numRows - 1) << 8) & 0x3F00;
2165 int curidx;
2166 ✗ if (m_addressingMode == ADDRESSING_MODE_ICND2153) {
2167 // ICND2153. Same {4,6,8,10,2} latch grammar as the FM6353 below, and
2168 // the same 0x0070 base for the scan register, but its own payload -
2169 // from kingdo9/rpi-rgb-led-matrix_pwm_experiment (branch
2170 // icnd2153_bsparacino, SPWM_ICND2153_REGISTER_ENTRIES), captured from
2171 // a 64x32 1/8 scan panel. Unlike the FM6353 this chip does carry
2172 // per-color current words in register 2.
2173 ✗ uint16_t r1 = 0x0070 | rn;
2174 ✗ curidx = outputRegData(0, odata, r1, r1, r1, m_numOutputSlots);
2175 ✗ curidx = outputRegData(curidx, odata, 0x7dfe, 0x71fe, 0x5dfe, m_numOutputSlots);
2176 ✗ curidx = outputRegData(curidx, odata, 0x4207, 0x4207, 0x4207, m_numOutputSlots);
2177 ✗ curidx = outputRegData(curidx, odata, 0x0040, 0x0040, 0x0040, m_numOutputSlots);
2178 ✗ curidx = outputRegData(curidx, odata, 0x0008, 0x0008, 0x0008, m_numOutputSlots);
2179 ✗ } else if (m_addressingMode == ADDRESSING_MODE_FM6353C) {
2180 // FM6353, values from DMD_STM32 (board707/DMD_STM32,
2181 // DMD_SPWM_Driver.h conf_6353 = {0x0008, 0x1f70, 0x6707, 0x40f7,
2182 // 0x0040} written in latch order 2,4,6,8,10). The latch length
2183 // selects the register, so the 4,6,8,10,2 order the PRU sends
2184 // (REG1..REG5) writes the same registers. No per-color current
2185 // data is known for this chip; brightness only comes from the
2186 // GCLK blanking time set below.
2187 ✗ uint16_t r1 = 0x0070 | rn;
2188 ✗ curidx = outputRegData(0, odata, r1, r1, r1, m_numOutputSlots);
2189 ✗ curidx = outputRegData(curidx, odata, 0x6707, 0x6707, 0x6707, m_numOutputSlots);
2190 ✗ curidx = outputRegData(curidx, odata, 0x40f7, 0x40f7, 0x40f7, m_numOutputSlots);
2191 ✗ curidx = outputRegData(curidx, odata, 0x0040, 0x0040, 0x0040, m_numOutputSlots);
2192 ✗ curidx = outputRegData(curidx, odata, 0x0008, 0x0008, 0x0008, m_numOutputSlots);
2193 } else {
2194 ✗ curidx = outputRegData(0, odata, conf_6363[0] | rn, conf_6363[1] | rn, conf_6363[2] | rn, m_numOutputSlots);
2195
2196 // register 2 contains adjustments for current
2197
2198 ✗ uint16_t b = 205; // stick with default brightness for now
2199 ✗ if (m_brightness >= 5) {
2200 ✗ b = (m_brightness - 5) * 10;
2201 ✗ if (m_brightness > 8) {
2202 ✗ b *= (245 - 64) * 2;
2203 } else {
2204 ✗ b *= (205 - 64) * 2;
2205 }
2206 ✗ b /= 100;
2207 ✗ b += 64;
2208 ✗ b <<= 1;
2209 ✗ b &= 0x1FE;
2210 ✗ b |= 0x200;
2211 } else {
2212 ✗ b = (m_brightness - 1) * 10;
2213 ✗ b *= 100;
2214 ✗ b /= 40;
2215 ✗ b *= (255 - 64);
2216 ✗ b /= 100;
2217 ✗ b += 64;
2218 ✗ b <<= 1;
2219 ✗ b &= 0x1FE;
2220 }
2221
2222 ✗ curidx = outputRegData(curidx, odata, conf_6363[3] | b, conf_6363[4] | b, conf_6363[5] | b, m_numOutputSlots);
2223 ✗ curidx = outputRegData(curidx, odata, conf_6363[6], conf_6363[7], conf_6363[8], m_numOutputSlots);
2224 ✗ curidx = outputRegData(curidx, odata, conf_6363[9], conf_6363[10], conf_6363[11], m_numOutputSlots);
2225 ✗ curidx = outputRegData(curidx, odata, conf_6363[12], conf_6363[13], conf_6363[14], m_numOutputSlots);
2226 }
2227
2228 ✗ pru->memcpyToPRU(&pruData->registers[0], &odata[0], curidx);
2229
2230 ✗ if (!pruIdle()) {
2231 ✗ return false;
2232 }
2233
2234 // Send the command to setup the registers
2235 ✗ setupGCLKConfig();
2236 ✗ pruData->numBlocks = rowLen / 16;
2237 ✗ pruData->numRows = numRows;
2238 ✗ pruData->cmd = PWM_COMMAND_REGISTERS;
2239 ✗ __asm__ __volatile__("" ::
2240 : "memory");
2241 ✗ return pruIdle();
2242 }
2243
2244 // Adopt an imported register profile, if the config carried one and this chip
2245 // is one whose upload grammar we drive from a PWMChipSeq. The grammar itself
2246 // (slot count, extra word, middle LAT burst, which register holds the scan
2247 // count) is a property of the chip, not of the capture, so it is taken from
2248 // the built-in entry and only the word lists are replaced.
2249 ✗ void BBShiftPanelManager::buildRegisterProfile(const PanelParams& p) {
2250 ✗ m_haveProfile = false;
2251 ✗ m_profName.clear();
2252 ✗ for (int c = 0; c < 3; c++) {
2253 ✗ m_profWords[c].clear();
2254 }
2255 ✗ if (p.regProfile[0].empty()) {
2256 ✗ return;
2257 }
2258 ✗ const PWMChipSeq* base = pwmChipSeqFor(m_addressingMode);
2259 ✗ if (!base) {
2260 // FM6353C/FM6363C drive their registers from a different code path
2261 // that has no PWMChipSeq to override, and the catalog stores those
2262 // two as the "fixed" payload type rather than "rgb".
2263 ✗ LogWarn(VB_CHANNELOUT, "BBShiftPanel: a register profile was configured but this panel type does not use one; ignoring it\n");
2264 ✗ return;
2265 }
2266 ✗ for (int c = 0; c < 3; c++) {
2267 ✗ m_profWords[c] = p.regProfile[c];
2268 }
2269 ✗ m_profSeq = *base;
2270 ✗ m_profSeq.r = m_profWords[0].data();
2271 ✗ m_profSeq.g = m_profWords[1].data();
2272 ✗ m_profSeq.b = m_profWords[2].data();
2273 ✗ m_profSeq.len = (int)m_profWords[0].size();
2274 // numRows is not known yet here (computeGeometry runs later), so the
2275 // scan-mismatch warning is suppressed at its own site via m_haveProfile
2276 // rather than by faking a defaultScan.
2277 ✗ m_profSeq.variants = nullptr;
2278 ✗ m_profSeq.variantCount = 0;
2279 ✗ m_haveProfile = true;
2280 ✗ m_profName = p.regProfileName;
2281 ✗ LogInfo(VB_CHANNELOUT, "BBShiftPanel: using imported register profile '%s' (%d words per color)\n",
2282 m_profName.c_str(), m_profSeq.len);
2283 }
2284
2285 ✗ const PWMChipSeq* BBShiftPanelManager::activePWMSeq() const {
2286 ✗ if (m_haveProfile) {
2287 ✗ return &m_profSeq;
2288 }
2289 ✗ return pwmChipSeqFor(m_addressingMode);
2290 }
2291
2292 ✗ void BBShiftPanelManager::writeFM6373SeqWord(int idx) {
2293 // rewrite the rotating register slot (slot 3 of 5) with sequence word
2294 // idx. Only safe while no register upload is in flight: called from
2295 // SendData after the previous DATA command was consumed (the PRU is
2296 // uploading frame data, which never reads the register slots, and the
2297 // previous REGISTERS upload completed before that DATA was dispatched)
2298 // and from the serialized init loop in setupPWMRegisters.
2299 ✗ const PWMChipSeq* seq = activePWMSeq();
2300 ✗ const uint16_t* sr = seq->r;
2301 ✗ const uint16_t* sg = seq->g;
2302 ✗ const uint16_t* sb = seq->b;
2303 ✗ for (int v = 0; v < seq->variantCount; v++) {
2304 ✗ if (seq->variants[v].scan == (int)numRows) {
2305 ✗ sr = seq->variants[v].r;
2306 ✗ sg = seq->variants[v].g;
2307 ✗ sb = seq->variants[v].b;
2308 ✗ break;
2309 }
2310 }
2311 ✗ uint16_t rw = sr[idx];
2312 ✗ uint16_t gw = sg[idx];
2313 ✗ uint16_t bw = sb[idx];
2314 ✗ if ((rw >> 8) == seq->scanReg) {
2315 // the scan row count lives in the low 6 bits; the upper 2 are a chip
2316 // constant carried over from the table (DMD_STM32 patches the same
2317 // entry for the FM6373)
2318 ✗ rw = gw = bw = (uint16_t)(((uint32_t)seq->scanReg << 8) | (rw & 0xC0) | ((numRows - 1) & 0x3F));
2319 }
2320 ✗ int slotSize = 16 * (m_numOutputSlots == 16 ? 12 : 6);
2321 uint8_t buf[192 * 2];
2322 ✗ int len = outputRegData(0, buf, rw, gw, bw, m_numOutputSlots);
2323 ✗ if (slotSize & 63) {
2324 // A slot that is not a whole number of 64 byte units shares a unit
2325 // with the one after it, so extend the write to cover that slot with
2326 // its own (constant) value rather than leave a partial unit: the
2327 // commit word for a five slot chip, the extra pre-commit word for a
2328 // six slot one.
2329 ✗ uint16_t next = seq->slots == 6 ? seq->extraWord : 0x0055;
2330 ✗ len = outputRegData(len, buf, next, next, next, m_numOutputSlots);
2331 }
2332 ✗ pru->memcpyToPRU((uint8_t*)&pruData->registers[0] + 2 * slotSize, buf, len);
2333 ✗ }
2334
2335 ✗ void BBShiftPanelManager::writeDP3364SeqWord(int idx) {
2336 // rewrite the single rotating register slot with sequence word idx. Same
2337 // in-flight rules as writeFM6373SeqWord: only safe while no register
2338 // upload is running.
2339 ✗ uint16_t rw = DP3364_SEQ_R[idx];
2340 ✗ uint16_t gw = DP3364_SEQ_G[idx];
2341 ✗ uint16_t bw = DP3364_SEQ_B[idx];
2342 ✗ if ((rw >> 8) == 0x02) {
2343 // scan row count
2344 ✗ rw = gw = bw = 0x0200 | ((numRows - 1) & 0x3F);
2345 ✗ } else if ((rw >> 8) == 0x08) {
2346 // linear output current multiplier. The captured 0x7f is the value
2347 // the panel was designed around, so it is full brightness and this
2348 // only ever scales down from it; the per-color trim in 0x09/0x0b is
2349 // left alone so the white balance does not move with brightness.
2350 ✗ uint16_t cur = std::clamp(0x7F * m_brightness / 10, 8, 0x7F);
2351 ✗ rw = gw = bw = 0x0800 | cur;
2352 }
2353 ✗ int slotSize = 16 * (m_numOutputSlots == 16 ? 12 : 6);
2354 uint8_t buf[192 * 2];
2355 ✗ int len = outputRegData(0, buf, rw, gw, bw, m_numOutputSlots);
2356 ✗ if (slotSize & 63) {
2357 // A slot that is not a whole number of 64 byte units shares a unit
2358 // with the one after it, so pad with a second copy of the word to keep
2359 // the write on a unit boundary. Nothing reads the padding - DP3364S
2360 // only sends one word per frame.
2361 ✗ len = outputRegData(len, buf, rw, gw, bw, m_numOutputSlots);
2362 }
2363 ✗ pru->memcpyToPRU((uint8_t*)&pruData->registers[0], buf, len);
2364 ✗ }
2365
2366 ✗ void BBShiftPanelManager::setupGCLKConfig() {
2367 // Configuration for the GCLK program on the other PRU (see the rowConfig
2368 // register in BBShiftPanel_gclk.asm): [0] = blanking loops between GCLK
2369 // packets (the brightness knob), [1] = row select mode, [2]/[3] = GCLK
2370 // pulses in the first packet after a restart / every packet after
2371 ✗ pwmPru->data_ram[0] = m_brightness > 8 ? 3 : 11 - m_brightness;
2372 ✗ if (m_addressingMode == ADDRESSING_MODE_DP3364) {
2373 // DP3364S generates its own GCLK from an internal PLL, so this side
2374 // only emits the per-row ROW strobe (which replaces OE on this chip)
2375 // and advances the row driver. Unlike the FM6373 the addressing
2376 // dropdown is honored: a panel with more than 32 scan rows cannot be
2377 // addressed by the 5 SEL lines at all and needs the token shift
2378 // register, which is what the row driver chips on these panels want.
2379 // Brightness comes from config register 0x08, not from blanking time.
2380 // [2] = W4 ROW pulse width in 100ns units (W12 is emitted as 3x
2381 // this; the chip only has to tell the two apart)
2382 // [3] = row period in us
2383 ✗ int dclkNs = m_numOutputSlots == 16 ? DCLK_NS_16 : DCLK_NS_8;
2384 ✗ int minLine = std::max({ dp3364MinLineDCLKs(DP3364_SEQ_R, DP3364_SEQ_LEN),
2385 ✗ dp3364MinLineDCLKs(DP3364_SEQ_G, DP3364_SEQ_LEN),
2386 ✗ dp3364MinLineDCLKs(DP3364_SEQ_B, DP3364_SEQ_LEN) });
2387 ✗ pwmPru->data_ram[1] = 4 | (m_pwmDirectRow ? 1 : 0);
2388 // W4 is 4 DCLK periods; the pulse is emitted as a time because the
2389 // DCLK comes from the other PRU with no phase relationship to this one
2390 ✗ pwmPru->data_ram[2] = std::clamp((4 * dclkNs + 99) / 100, 2, 40);
2391 // The datasheet's line time is a minimum, and the row scan free runs,
2392 // so a row that is too short simply will not have finished displaying.
2393 // 2x the minimum is the margin for that; the scan still revisits every
2394 // row far faster than the eye at any supported scan count.
2395 ✗ pwmPru->data_ram[3] = std::clamp((2 * minLine * dclkNs + 999) / 1000, 4, 255);
2396 // [4] = PWM display group count, config register 0x03 + 1. One W12
2397 // ROW pulse is emitted per (groups x rows) cycle, so this has to
2398 // track whatever DP3364_SEQ_* sets 0x03 to.
2399 ✗ pwmPru->data_ram[4] = (DP3364_SEQ_R[1] & 0x7F) + 1;
2400 ✗ return;
2401 }
2402 ✗ if (activePWMSeq()) {
2403 // FM6373 family: single OE pulse per row. kingdo9 gives ICND1065L
2404 // and SM16380SH the same OE style, so they run this scan too.
2405 // Brightness comes from the chip's config registers, not the blanking
2406 // time. [2] = opener pulse width us, [3] = row period us (~128 DCLKs
2407 // at the data clock rate, so the scan rate is about the same while
2408 // uploading and while free-running between frames)
2409 //
2410 // Row transport: the DP32019B boards this family was written against
2411 // put a binary row number on SEL0-4, and that stayed the only choice
2412 // here for a while. Some ICND1065L panels instead wire A/B/C to a
2413 // token shift register with D/E grounded (GH #2955), which is the
2414 // only way to reach more scan rows than the SEL lines can count.
2415 // Direct stays the default so those existing boards are untouched;
2416 // bit 0 clear picks the shift register and only an explicit ABC Shift
2417 // addressing selection does that.
2418 ✗ pwmPru->data_ram[1] = 2 | (m_pwmShiftRow ? 0 : 1);
2419 // OE timing. The reference implementation denominates these in DCLK
2420 // periods rather than in time, and they are the first thing its users
2421 // tune on a panel that will not light, so take DCLK counts here too
2422 // and convert. The defaults are its defaults (oe_clk_length 4,
2423 // first_oe_clk_length 12), which at either data clock rate land on
2424 // about the fixed 600ns / 2us this used to emit.
2425 ✗ int dclk = m_numOutputSlots == 16 ? DCLK_NS_16 : DCLK_NS_8;
2426 ✗ pwmPru->data_ram[2] = m_oeFirstClkLength
2427 ✗ ? std::clamp((m_oeFirstClkLength * dclk + 999) / 1000, 1, 255)
2428 : 2;
2429 ✗ pwmPru->data_ram[3] = m_oeRowPeriodUs;
2430 // byte 5 is in 100ns units; see oeWidth in BBShiftPanel_gclk.asm
2431 ✗ pwmPru->data_ram[5] = m_oeClkLength
2432 ✗ ? std::clamp((m_oeClkLength * dclk + 99) / 100, 1, 255)
2433 : 6;
2434 ✗ LogDebug(VB_CHANNELOUT, "BBShiftPanel: OE pulse %d (100ns units), opener %dus, row period %dus (DCLK %dns)\n",
2435 pwmPru->data_ram[5], pwmPru->data_ram[2], pwmPru->data_ram[3], dclk);
2436 ✗ return;
2437 }
2438 ✗ pwmPru->data_ram[1] = m_pwmDirectRow ? 1 : 0;
2439 ✗ if (m_addressingMode == ADDRESSING_MODE_FM6353C ||
2440 ✗ m_addressingMode == ADDRESSING_MODE_ICND2153) {
2441 // DMD_STM32 GCLK_NUM: FM6353 row switching takes 138 GCLK pulses
2442 // (uniform; no first-packet extension is documented for this chip).
2443 // kingdo9's ICND2153 capture independently lands on 138 pulses per
2444 // scan, though it drives them as a half-rate waveform (one pulse per
2445 // two clock slots) that this GCLK program does not reproduce.
2446 ✗ pwmPru->data_ram[2] = 138;
2447 ✗ pwmPru->data_ram[3] = 138;
2448 } else {
2449 // FM6363: 74 pulses per row, first packet after a restart is 78
2450 // (logic analyzer capture; independently confirmed by the
2451 // kingdo9/rpi-rgb-led-matrix_pwm_experiment measurements)
2452 ✗ pwmPru->data_ram[2] = 78;
2453 ✗ pwmPru->data_ram[3] = 74;
2454 }
2455 }
2456
2457 ✗ uint32_t BBShiftPanelManager::computeMaxBrightnessCycles() {
2458 // Scale the row length by the amount of data actually shifted per pixel
2459 // clock: 16 output slots shift twice the bytes of 8 slots, so the same
2460 // physical row takes twice as long to shift out
2461 ✗ uint32_t effLen = rowLen * m_numOutputSlots / 8;
2462 // Longer rows take longer to shift, so they get a longer maximum on-time
2463 // to keep the duty cycle (brightness) up at the cost of refresh rate.
2464 // This is a smooth ramp so that adding one panel cannot step the
2465 // brightness; it passes through 0x8800 at 256, 0xA800 at 384, 0xC800 at
2466 // 512 and 0xE800 at 640 (the values the old stepped defaults used).
2467 ✗ uint32_t maxBright = 64 * effLen + 18432;
2468 // The ramp and its bounds are tuned for 16 scan rows. With fewer rows
2469 // each row owns a larger share of the frame so the on-times may grow
2470 // (1/8 scan); with more rows they must shrink or the refresh tanks
2471 // (1/32 scan P2.5 panels) - scale the bounds by 16/numRows so all scan
2472 // ratios get the same refresh contract.
2473 ✗ maxBright = std::clamp(maxBright, 0x8800u * 16 / numRows, 0xE800u * 16 / numRows);
2474 ✗ if (FileExists(FPP_DIR_MEDIA("/config/panel_timing.txt"))) {
2475 ✗ std::string v = GetFileContents(FPP_DIR_MEDIA("/config/panel_timing.txt"));
2476 ✗ if (!v.empty()) {
2477 ✗ maxBright = std::stoi(v, nullptr, 16);
2478 }
2479 ✗ }
2480 ✗ return maxBright;
2481 }
2482
2483 ✗ void BBShiftPanelManager::buildStrideSchedule() {
2484 ✗ m_strideSchedule.clear();
2485 ✗ m_dupCopies.clear();
2486
2487 ✗ uint32_t maxBright = computeMaxBrightnessCycles();
2488 // Approximate PRU cycles to shift one full stride out to the panels
2489 // (per-pixel cost including the amortized data load overhead). In the
2490 // two-PRU configuration the OE PRU prefetches the ring blocks and hands
2491 // them over through the scratchpad, which shortens the load (measured:
2492 // 80.0 -> 83.2Hz on a 4-panel 12-bit chain, ~42 cycles/pixel effective)
2493 ✗ bool pruPrefetch = !singlePRU && !isPWMPanel();
2494 ✗ uint32_t cyclesPerPixel = (m_numOutputSlots == 16) ? (pruPrefetch ? 42 : 48)
2495 : (pruPrefetch ? 25 : 26);
2496 ✗ uint32_t shiftCycles = rowLen * cyclesPerPixel + 100;
2497
2498 // Capacity limit: the PRU brightness table holds 768 stride entries
2499 // (24 slots at 32 rows, so 1/32 scan panels keep a split-pulse budget)
2500 ✗ uint32_t bytesPerPixel = (m_numOutputSlots * 6 * 2) / 16;
2501 ✗ uint32_t strideLen = rowLen * bytesPerPixel;
2502 ✗ int maxSlots = 768 / (int)numRows;
2503 ✗ if (maxSlots < m_colorDepth) {
2504 // no room for splitting, the base schedule has to fit regardless
2505 ✗ maxSlots = m_colorDepth;
2506 }
2507
2508 // Decide how many pulses each bit is displayed as. A bit whose on-time
2509 // is >= k * shift time can be shown as k shorter pulses spread across the
2510 // frame at zero cost in frame time (the shifting still fits under each
2511 // pulse), which multiplies the perceived refresh rate. The count rounds
2512 // to nearest: pieces slightly shorter than the shift time cost a little
2513 // dead time (bounded by half a shift per bit) but move that bit's light
2514 // to a much higher frequency - on longer chains the second MSB would
2515 // otherwise sit unsplit at the frame rate carrying a quarter of the light.
2516 ✗ int budget = maxSlots - m_colorDepth;
2517 ✗ std::vector<int> pieces(m_colorDepth, 1);
2518 ✗ for (int b = m_colorDepth - 1; b >= 0 && budget > 0; b--) {
2519 ✗ uint32_t on = (m_brightness * maxBright / 10) >> (m_colorDepth - 1 - b);
2520 ✗ int k = std::min((int)((on + shiftCycles / 2) / shiftCycles), 4);
2521 ✗ k = std::min(k, budget + 1);
2522 ✗ if (k <= 1) {
2523 // lower bits have shorter on-times, no more splitting is useful
2524 ✗ break;
2525 }
2526 ✗ pieces[b] = k;
2527 ✗ budget -= k - 1;
2528 }
2529
2530 // Place the pieces on the timeline. Each bit's pieces are spread evenly
2531 // across the frame with a half-spacing phase offset so that every pixel
2532 // value, not just full white, sees its light distributed across the whole
2533 // frame (e.g. 4 pieces land at 1/8, 3/8, 5/8, 7/8 and interleave with the
2534 // 2-piece bit at 1/4, 3/4). Bits place MSB first; a collision moves to
2535 // the nearest free slot.
2536 ✗ int n = 0;
2537 ✗ for (int b = 0; b < m_colorDepth; b++) {
2538 ✗ n += pieces[b];
2539 }
2540 ✗ m_strideSchedule.assign(n, StrideSlot{ 0, false, 0 });
2541 ✗ std::vector<bool> used(n, false);
2542 ✗ for (int b = m_colorDepth - 1; b >= 0; b--) {
2543 ✗ uint32_t on = (m_brightness * maxBright / 10) >> (m_colorDepth - 1 - b);
2544 ✗ uint32_t per = on / pieces[b];
2545 ✗ for (int i = 0; i < pieces[b]; i++) {
2546 ✗ int target = (int)(((i + 0.5f) * n) / pieces[b]);
2547 ✗ int slot = target;
2548 ✗ for (int d = 0; d < n; d++) {
2549 // search outward from the target, alternating below/above
2550 ✗ int cand = (target + ((d & 1) ? (n - (d + 1) / 2) : (d / 2))) % n;
2551 ✗ if (!used[cand]) {
2552 ✗ slot = cand;
2553 ✗ break;
2554 }
2555 }
2556 ✗ used[slot] = true;
2557 // keep the total on-time exact, first piece takes the remainder
2558 ✗ uint32_t t = (i == 0) ? (on - per * (pieces[b] - 1)) : per;
2559 ✗ m_strideSchedule[slot] = { (uint8_t)b, false, t };
2560 }
2561 }
2562 ✗ bool seen[16] = { false };
2563 ✗ for (auto& slot : m_strideSchedule) {
2564 ✗ slot.primary = !seen[slot.bit];
2565 ✗ seen[slot.bit] = true;
2566 }
2567
2568 // Frame buffer copies needed for the non-primary (duplicated) strides
2569 ✗ m_dupCopies.assign(numRows, {});
2570 int primarySlot[16];
2571 ✗ for (int s = 0; s < n; s++) {
2572 ✗ if (m_strideSchedule[s].primary) {
2573 ✗ primarySlot[m_strideSchedule[s].bit] = s;
2574 }
2575 }
2576 ✗ for (int s = 0; s < n; s++) {
2577 ✗ if (m_strideSchedule[s].primary) {
2578 ✗ continue;
2579 }
2580 ✗ int ps = primarySlot[m_strideSchedule[s].bit];
2581 ✗ for (int r = 0; r < numRows; r++) {
2582 ✗ uint32_t dstIdx = m_outputByRow ? (r * n + s) : (s * numRows + r);
2583 ✗ uint32_t srcIdx = m_outputByRow ? (r * n + ps) : (ps * numRows + r);
2584 ✗ m_dupCopies[r].emplace_back(dstIdx * strideLen, srcIdx * strideLen);
2585 }
2586 }
2587
2588 // log what this schedule works out to
2589 ✗ uint64_t rowCycles = 0;
2590 ✗ uint64_t onCycles = 0;
2591 ✗ for (auto& slot : m_strideSchedule) {
2592 ✗ rowCycles += std::max(shiftCycles, slot.onTime) + 60;
2593 ✗ onCycles += slot.onTime;
2594 }
2595 ✗ uint64_t frameCycles = rowCycles * numRows;
2596 ✗ float refresh = 250000000.0f / (float)frameCycles;
2597 ✗ LogInfo(VB_CHANNELOUT, "BBShiftPanel: %d strides/row (%d bit color, MSB split %d ways), est refresh %d Hz, duty %d%%\n",
2598 n, m_colorDepth, pieces[m_colorDepth - 1], (int)refresh, (int)(onCycles * 100 / rowCycles));
2599 // a rebuild with other geometry replaces the estimate, so the banner for
2600 // the previous one has to come down first or it stays up for good
2601 ✗ if (!m_refreshWarning.empty()) {
2602 ✗ WarningHolder::RemoveWarning(m_refreshWarning);
2603 ✗ m_refreshWarning.clear();
2604 }
2605 ✗ if (refresh < 60.0f) {
2606 ✗ m_refreshWarning = "LED panel refresh rate is only " + std::to_string((int)refresh) + "Hz and may flicker; reduce the color depth or panels per output";
2607 ✗ WarningHolder::AddWarning(m_refreshWarning);
2608 }
2609 ✗ }
2610
2611 ✗ void BBShiftPanelManager::setupBrightnessValues() {
2612 ✗ uint32_t* cur = &pruData->brightness[0];
2613 ✗ int n = m_strideSchedule.size();
2614 ✗ auto writeEntry = [&](int s, int r) {
2615 ✗ int mappedRow = mapRow(r, m_addressingMode);
2616 ✗ cur[0] = m_strideSchedule[s].onTime;
2617 ✗ cur[1] = (mappedRow << 24) & 0x7F000000;
2618 ✗ if (m_outputByRow && m_outputBlankData && (s == 0)) {
2619 ✗ cur[1] |= 0x80000000;
2620 }
2621 // printf("Brightness[%d %d] = %08x %08x\n", s, r, cur[0], cur[1]);
2622 ✗ cur += 2;
2623 ✗ };
2624 ✗ if (m_outputByRow) {
2625 ✗ for (int r = 0; r < numRows; r++) {
2626 ✗ for (int s = 0; s < n; s++) {
2627 ✗ writeEntry(s, r);
2628 }
2629 }
2630 } else {
2631 ✗ for (int s = 0; s < n; s++) {
2632 ✗ for (int r = 0; r < numRows; r++) {
2633 ✗ writeEntry(s, r);
2634 }
2635 }
2636 }
2637 ✗ }
2638
2639 // Maps this matrix's channels onto the shared frame. The lane a pixel lands
2640 // in is fixed by which cape output its panel is on (outputPin/outputBank from
2641 // the cape pinout), which is what lets two matrices share one frame without
2642 // coordinating: they simply never compute the same destination offset.
2643 ✗ bool BBShiftPanelOutput::buildScatterMap() {
2644 ✗ auto& mgr = BBShiftPanelManager::INSTANCE;
2645 ✗ const int m_panelWidth = mgr.m_panelWidth;
2646 ✗ const int m_panelHeight = mgr.m_panelHeight;
2647 ✗ const uint32_t rowLen = mgr.rowLen;
2648 ✗ const uint32_t maxRowLen = mgr.maxRowLen;
2649 ✗ const int m_numOutputSlots = mgr.m_numOutputSlots;
2650
2651 // Full height layouts map every row directly and have no interleave; a
2652 // handler for them cannot exist (scan == height leaves no room for the
2653 // folded columns it checks for), so only the split layout asks for one.
2654 ✗ PanelInterleaveHandler* handler = nullptr;
2655 ✗ if (!mgr.m_dataLayout) {
2656 ✗ handler = PanelInterleaveHandler::createHandler(mgr.m_panelInterleave, m_panelWidth, m_panelHeight, mgr.m_panelScan);
2657 ✗ if (!handler) {
2658 ✗ LogErr(VB_CHANNELOUT, "Failed to create panel interleave handler\n");
2659 ✗ return false;
2660 }
2661 }
2662
2663 ✗ uint32_t* channelOffsets = new uint32_t[m_channelCount];
2664 ✗ memset(channelOffsets, 0xFF, m_channelCount * sizeof(uint32_t));
2665
2666 ✗ int pixelStride = m_numOutputSlots * 6;
2667 ✗ int totalRowLen = rowLen * pixelStride;
2668 ✗ for (int output = 0; output < mgr.m_numOutputs; output++) {
2669 ✗ int panelsOnOutput = m_panelMatrix->m_outputPanels[output].size();
2670 // For 16 outputs, bank1 data starts at offset 48 (= 8 slots * 6 colors)
2671 // so that ISPC's 16-lane reduce_add packs bank0/bank1 pairs correctly:
2672 // bank0 channels occupy slots 0-47, bank1 channels occupy slots 48-95
2673 ✗ int outputIdx = mgr.outputPin[output] + mgr.outputBank[output] * 48;
2674
2675 ✗ for (int i = 0; i < panelsOnOutput; i++) {
2676 ✗ int panel = m_panelMatrix->m_outputPanels[output][i];
2677 ✗ int c = m_panelMatrix->m_panels[panel].chain;
2678 // the chain position is measured from the far end of the LONGEST
2679 // chain on the cape, so a matrix with a shorter chain than another
2680 // simply shifts the leading pixels off the end of its own chain
2681 ✗ int chain = mgr.m_longestChain - c - 1;
2682 ✗ int xOff = chain * maxRowLen;
2683
2684 ✗ if (mgr.m_dataLayout) {
2685 // Full height layout: lane 1 and lane 2 carry the two halves
2686 // of the SAME row. Layout 2 puts the left half on lane 1,
2687 // layout 1 swaps them. Every row is its own scan address, so
2688 // yOut is just y and the row is half a panel wide.
2689 ✗ const int half = m_panelWidth / 2;
2690 ✗ const int leftLane1 = (mgr.m_dataLayout == 2);
2691 ✗ for (int y = 0; y < m_panelHeight; y++) {
2692 ✗ int yw = y * m_panelWidth * 3;
2693 ✗ for (int x = 0; x < half; ++x) {
2694 ✗ int lane1x = leftLane1 ? x : x + half;
2695 ✗ int lane2x = leftLane1 ? x + half : x;
2696 ✗ uint32_t r1 = m_panelMatrix->m_panels[panel].pixelMap[yw + lane1x * 3];
2697 ✗ uint32_t g1 = m_panelMatrix->m_panels[panel].pixelMap[yw + lane1x * 3 + 1];
2698 ✗ uint32_t b1 = m_panelMatrix->m_panels[panel].pixelMap[yw + lane1x * 3 + 2];
2699
2700 ✗ uint32_t r2 = m_panelMatrix->m_panels[panel].pixelMap[yw + lane2x * 3];
2701 ✗ uint32_t g2 = m_panelMatrix->m_panels[panel].pixelMap[yw + lane2x * 3 + 1];
2702 ✗ uint32_t b2 = m_panelMatrix->m_panels[panel].pixelMap[yw + lane2x * 3 + 2];
2703
2704 ✗ int yOut = y;
2705 ✗ int xOut = x + xOff;
2706 ✗ if (mgr.isPWMPanel()) {
2707 ✗ int xo2 = xOut % 16;
2708 ✗ int xo3 = xOut / 16;
2709 ✗ xOut = xo2 * (rowLen / 16) + xo3;
2710 }
2711 ✗ channelOffsets[r1] = yOut * totalRowLen + xOut * pixelStride + outputIdx;
2712 ✗ channelOffsets[g1] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 8;
2713 ✗ channelOffsets[b1] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 16;
2714 ✗ channelOffsets[r2] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 24;
2715 ✗ channelOffsets[g2] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 32;
2716 ✗ channelOffsets[b2] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 40;
2717 }
2718 }
2719 ✗ continue;
2720 ✗ }
2721
2722 ✗ for (int y = 0; y < (m_panelHeight / 2); y++) {
2723 ✗ int yw1 = y * m_panelWidth * 3;
2724 ✗ int yw2 = (y + (m_panelHeight / 2)) * m_panelWidth * 3;
2725
2726 ✗ for (int x = 0; x < m_panelWidth; ++x) {
2727 ✗ uint32_t r1 = m_panelMatrix->m_panels[panel].pixelMap[yw1 + x * 3];
2728 ✗ uint32_t g1 = m_panelMatrix->m_panels[panel].pixelMap[yw1 + x * 3 + 1];
2729 ✗ uint32_t b1 = m_panelMatrix->m_panels[panel].pixelMap[yw1 + x * 3 + 2];
2730
2731 ✗ uint32_t r2 = m_panelMatrix->m_panels[panel].pixelMap[yw2 + x * 3];
2732 ✗ uint32_t g2 = m_panelMatrix->m_panels[panel].pixelMap[yw2 + x * 3 + 1];
2733 ✗ uint32_t b2 = m_panelMatrix->m_panels[panel].pixelMap[yw2 + x * 3 + 2];
2734 ✗ int yOut = y;
2735 ✗ int xOut = x;
2736 ✗ handler->map(xOut, yOut);
2737 ✗ xOut += xOff;
2738
2739 ✗ if (mgr.isPWMPanel()) {
2740 // For PWM panels, the first of each group of 16 pixels is out first,
2741 // then the second of each group of 16, etc...
2742 ✗ int xo2 = xOut % 16;
2743 ✗ int xo3 = xOut / 16;
2744
2745 ✗ xOut = xo2 * (rowLen / 16) + xo3;
2746 }
2747 // Color stride is always 8: within each bank, 8 slots per color channel.
2748 // For 8 outputs: outputIdx=pin(0-7), channels at +0,+8,+16,+24,+32,+40
2749 // For 16 outputs bank0: same; bank1: outputIdx=pin+48, same offsets
2750 ✗ channelOffsets[r1] = yOut * totalRowLen + xOut * pixelStride + outputIdx;
2751 ✗ channelOffsets[g1] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 8;
2752 ✗ channelOffsets[b1] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 16;
2753 ✗ channelOffsets[r2] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 24;
2754 ✗ channelOffsets[g2] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 32;
2755 ✗ channelOffsets[b2] = yOut * totalRowLen + xOut * pixelStride + outputIdx + 40;
2756 }
2757 }
2758 }
2759 }
2760 ✗ delete handler;
2761
2762 // Build the scatter map sorted by destination offset so the prep loop
2763 // writes sequentially; the random accesses become byte reads, which the
2764 // cache handles far better than random read-modify-write stores.
2765 // Channels not covered by any panel (partially invalid layouts, unused
2766 // canvas regions) still have the 0xFFFFFFFF fill from above and are left
2767 // out of the map; writing through the marker used to crash. The stable
2768 // sort keeps the original channel order for duplicated offsets so the
2769 // last writer still wins.
2770 ✗ std::vector<std::pair<uint32_t, uint32_t>> map;
2771 ✗ map.reserve(m_channelCount);
2772 ✗ uint32_t unmapped = 0;
2773 ✗ for (int x = 0; x < m_channelCount; x++) {
2774 ✗ if (channelOffsets[x] != 0xFFFFFFFF) {
2775 ✗ map.emplace_back(channelOffsets[x], x);
2776 } else {
2777 ✗ ++unmapped;
2778 }
2779 }
2780 ✗ if (unmapped) {
2781 ✗ LogWarn(VB_CHANNELOUT, "BBShiftPanel: %u of %u channels are not mapped to any panel; check the panel layout\n",
2782 unmapped, m_channelCount);
2783 }
2784 ✗ std::stable_sort(map.begin(), map.end(),
2785 ✗ [](const std::pair<uint32_t, uint32_t>& a, const std::pair<uint32_t, uint32_t>& b) { return a.first < b.first; });
2786 ✗ m_scatterOffsets.resize(map.size());
2787 ✗ m_scatterSrc.resize(map.size());
2788 ✗ for (size_t x = 0; x < map.size(); x++) {
2789 ✗ m_scatterOffsets[x] = map[x].first;
2790 ✗ m_scatterSrc[x] = map[x].second;
2791 }
2792 ✗ delete[] channelOffsets;
2793 ✗ return true;
2794 ✗ }
2795
2796 ✗ void BBShiftPanelOutput::setupGamma() {
2797 ✗ auto& mgr = BBShiftPanelManager::INSTANCE;
2798 // The OE on-time is shared by every matrix on the cape, so a matrix that
2799 // asked for less brightness than the cape ended up running at makes up the
2800 // difference here. With one matrix, or with matrices that agree, the
2801 // ratio is 1.0 and the table is unchanged.
2802 ✗ float brightnessScale = 1.0f;
2803 ✗ if (mgr.m_brightness > 0 && m_brightness < mgr.m_brightness) {
2804 ✗ brightnessScale = (float)m_brightness / (float)mgr.m_brightness;
2805 }
2806
2807 ✗ int colorDepth = mgr.m_colorDepth;
2808 ✗ if (mgr.isPWMPanel()) {
2809 // we are outputting 16 bit data as that's what the
2810 // PWM registers require, but only the bottom 12 bits are used
2811 ✗ colorDepth = 12;
2812 }
2813
2814 ✗ GammaLUT::BuildForColorDepth(gammaCurve, GammaLUT::Clamp(m_gamma, 2.2f), colorDepth, brightnessScale);
2815 /*
2816 for (int x = 0; x < 256; x++) {
2817 printf("%d: %04x\n", x, gammaCurve[x]);
2818 }
2819 */
2820 ✗ }
2821
2822 ✗ void BBShiftPanelOutput::OverlayTestData(unsigned char* channelData, int cycleNum, float percentOfCycle, int testType, const Json::Value& config) {
2823 ✗ for (int output = 0; output < BBShiftPanelManager::INSTANCE.numOutputs(); output++) {
2824 ✗ int panelsOnOutput = m_panelMatrix->m_outputPanels[output].size();
2825 ✗ for (int i = 0; i < panelsOnOutput; i++) {
2826 ✗ int panel = m_panelMatrix->m_outputPanels[output][i];
2827
2828 ✗ m_panelMatrix->m_panels[panel].drawTestPattern(channelData + m_startChannel, cycleNum, percentOfCycle, testType);
2829 }
2830 }
2831 ✗ }
2832
2833 ✗ void BBShiftPanelOutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) {
2834 ✗ addRange(m_startChannel, m_startChannel + m_channelCount - 1);
2835 ✗ }
2836
2837 ✗ void BBShiftPanelOutput::DumpConfig(void) {
2838 ✗ LogDebug(VB_CHANNELOUT, "BBShiftPanelOutput::DumpConfig()\n");
2839 ✗ LogDebug(VB_CHANNELOUT, " Width : %d\n", m_width);
2840 ✗ LogDebug(VB_CHANNELOUT, " Height : %d\n", m_height);
2841 ✗ LogDebug(VB_CHANNELOUT, " Longest Chain : %d\n", m_longestChain);
2842 ✗ LogDebug(VB_CHANNELOUT, " Inverted Data : %d\n", m_invertedData);
2843 ✗ LogDebug(VB_CHANNELOUT, " Brightness : %d\n", m_brightness);
2844 ✗ LogDebug(VB_CHANNELOUT, " Gamma : %f\n", m_gamma);
2845 ✗ BBShiftPanelManager::INSTANCE.dumpConfig();
2846
2847 ✗ ChannelOutput::DumpConfig();
2848 ✗ }
2849
2850 ✗ void BBShiftPanelManager::dumpConfig() {
2851 ✗ LogDebug(VB_CHANNELOUT, " Shared cape config (%d matri%s):\n",
2852 (int)m_members.size(), m_members.size() == 1 ? "x" : "ces");
2853 ✗ LogDebug(VB_CHANNELOUT, " Panel Width : %d\n", m_panelWidth);
2854 ✗ LogDebug(VB_CHANNELOUT, " Panel Height : %d\n", m_panelHeight);
2855 ✗ LogDebug(VB_CHANNELOUT, " Color Depth : %d\n", m_colorDepth);
2856 ✗ LogDebug(VB_CHANNELOUT, " Longest Chain : %d\n", m_longestChain);
2857 ✗ LogDebug(VB_CHANNELOUT, " Cape Brightness: %d\n", m_brightness);
2858 ✗ LogDebug(VB_CHANNELOUT, " Output Rows : %d\n", numRows);
2859 ✗ LogDebug(VB_CHANNELOUT, " Output Length : %d\n", rowLen);
2860 ✗ LogDebug(VB_CHANNELOUT, " Num Outputs : %d (%d slots)\n", m_numOutputs, m_numOutputSlots);
2861 ✗ LogDebug(VB_CHANNELOUT, " Addressing Mode: %d %s\n", m_addressingMode, isPWMPanel() ? "PWM" : "Shift");
2862 ✗ if (!m_strideSchedule.empty()) {
2863 ✗ std::string sched;
2864 ✗ for (auto& slot : m_strideSchedule) {
2865 ✗ sched += " " + std::to_string(slot.bit) + (slot.primary ? "" : "*") + "/" + std::to_string(slot.onTime);
2866 }
2867 ✗ LogDebug(VB_CHANNELOUT, " Stride Schedule:%s\n", sched.c_str());
2868 ✗ }
2869 ✗ }
2870