Hallo Forum, ich möchte die WS2812B Library von Martin Hubáček von STM32F302 auf STM32F103C8 adaptieren. Leider bereiten mir die DMA Einstellungen Probleme. Meine Adaption ist angehängt. Das Original gibt es hier: https://github.com/hubmartin/ws2812b_stm32F3 Mir schien die F3 Version näher am F1 als die F4 Version. Mein Problem konkret ist, dass am PWM Pin (TIM1_CH1, GPIOA PIN8) nur ein high Signal (5V) raus kommt (durch einen externen Pullup nach 5V und open drain Ausgang). Die PWM tut gar nicht, was vermutlich mit der DMA Konfiguration zu tun hat. Ich glaube ich habe die DMA channel für Timer update und CC1 richtig adaptiert. RM0008 Seite 282. Aber was mache ich mit CC2? Es gibt keinen DMA channel für TIM1_CH2. Ich habe es mit DMA channel4 probiert (TIM1_CH4, TIM1_TRIG, TIM1_COM). So ganz klar ist mir die Aufgabe von CC2 aber auch nicht. Ich denke es ist für das DMA Doublebuffering gedacht. Kann mir jemand auf die Sprünge helfen? Danke.
Eine fertige und funktionierende Lib für STM32F103C8 und für einige STM32F4xx findest Du im WordClock-Projekt: https://www.mikrocontroller.net/articles/WordClock_mit_WS2812 Repo-Browser, d.h. direkter Link zu den WS2812-Sourcen: https://www.mikrocontroller.net/svnbrowser/wordclock24h/src/ws2812/ Benutzt wird DMA und Double-Buffering bei minimalem DMA-Buffer-Verbrauch. Der DMA-Buffer ist lediglich 48 Bytes lang statt viele KB (je nach Länge der verwendeten Stripes).
:
Bearbeitet durch Moderator
Frank M. schrieb: > Eine fertige und funktionierende Lib für STM32F103C8 und für einige > STM32F4xx findest Du im WordClock-Projekt Hallo Frank, danke für deine Antwort. Das WordClock Projekt kenne ich. Ich hatte etwas für die HAL Libs gesucht, da mein Projekt schon darauf basiert. Aber evtl. ist es einfacher deine Version auf HAL umzustricken. Gruß Bernd
Bernd schrieb: > Ich hatte etwas für die HAL Libs gesucht, da mein Projekt schon darauf > basiert Interessant. Ich dachte bisher, die HAL-Libs würden die Hardware derart abstrahieren, dass eine Portierung wesentlich einfacher zu machen wäre. Hm. Drücke Dir die Daumen.
Frank M. schrieb: > Ich dachte bisher, die HAL-Libs würden die Hardware derart > abstrahieren, dass eine Portierung wesentlich einfacher zu machen wäre. Ich finde es dadurch zum Teil wesentlich schwieriger, weil nicht immer klar ist, was eigentlich genau passiert. Das sich einige Strukturen zur Initialisierung der Peripherie deutlich von den Standard Peripheral Libs unterscheiden, macht es nicht unbedingt einfacher. Irgendwie komme ich auch mit der HAL Doku nicht zurecht. Ohne Beispiele ist es sehr schwer für mich. Was dagegen gut funktioniert, ist eine Grundkonfiguration mit CubeMX. Auch wenn die Aufteilung der Projektdateien nicht nach meinem Geschmack ist. Dein Projekt ist sehr schön strukturiert finde ich.
Ich habe mal für den F4 eine Ansteuerung basierend auf der HAL geschrieben. Ausgabe der Daten direkt über DMA/PWM; hier hatte ich das m.E. beste aus den vorhandenen Lösungen zusammengesucht. Es ist lediglich ein Zwischenspeicher für die Werte notwendig. (Ich hatte den Zwischenspeicher sogar verdoppelt, um Flackern bei der Ausgabe zu vermeiden - wenn gleichzeitig auf den Puffer gelesen und geschrieben wird). Im DMA-Speicher sind die Daten für zwei LEDs enthalten, somit kann immer einer aus dem Display-Zwischenspeicher geladen werden. Die Streifen waren dabei zu einer Matrix angeordnet - dies lässt sich aber parametrisieren. Die Matrix ist prinzipiell wie folgt aufgebaut: ---------------| | |--------------| | |--------------| | ---------------| etc. Defines:
1 | // WS2812b Defines
|
2 | #define PWM_ZERO 32
|
3 | #define PWM_ONE 64
|
4 | |
5 | #define RED 1
|
6 | #define GREEN 0
|
7 | #define BLUE 2
|
8 | |
9 | #define DISPX 90
|
10 | #define DISPY 6
|
11 | #define COLORS 3
|
12 | |
13 | #define DMABUFSIZE 48 // 2 LED Dataframes á 24 "Bits"
|
Benötigte Variablen:
1 | uint8_t display_buffer[DISPY][DISPX][COLORS] = {0}; //max Display size: 255x255! |
2 | uint8_t display_buffer_show[DISPY][DISPX][COLORS] = {0}; //double buffer for anti-flicker |
3 | |
4 | uint8_t x_index = 0; |
5 | uint8_t y_index = 0; |
6 | uint8_t res_index = 0; |
7 | |
8 | // reset_threshold: single dataset length: 1.25us*24 = 30us, min reset pulse 50 us -> res_th = 3
|
9 | uint8_t res_th = 3; |
10 | |
11 | |
12 | // DMA_Buffer -> 2x LED Daten; Wechsel bei Half complete IRQ
|
13 | uint16_t dma_buffer[DMABUFSIZE] = {0}; |
14 | //uint16_t resetpattern[24] = {0};
|
15 | |
16 | // Buffer state index -> which dataset is next in Buffer
|
17 | uint32_t bufcount = 0; |
18 | // frame size
|
19 | uint32_t displaydots = DISPY * DISPX; |
Folgende Routinen müssen implementiert werden: (fill_area ist nicht unbedingt notwendig, zwigt aber den Zugriff auf den Zwischenspeicher
1 | void dma_callback_halfcomplete(DMA_HandleTypeDef *_hdma) |
2 | {
|
3 | if(x_index < DISPX && y_index < DISPY) // 0-based index! |
4 | {
|
5 | // load frame into lower dma_buffer
|
6 | // for each bit, the correspondig pwm value is set
|
7 | for (uint8_t icol = 0; icol<COLORS; icol++) |
8 | {
|
9 | // test each bit / send high bit first
|
10 | dma_buffer[icol*8 + 0] = ((display_buffer_show[y_index][x_index][icol] & 0b10000000)>0 ? PWM_ONE : PWM_ZERO ); |
11 | dma_buffer[icol*8 + 1] = ((display_buffer_show[y_index][x_index][icol] & 0b01000000)>0 ? PWM_ONE : PWM_ZERO ); |
12 | dma_buffer[icol*8 + 2] = ((display_buffer_show[y_index][x_index][icol] & 0b00100000)>0 ? PWM_ONE : PWM_ZERO ); |
13 | dma_buffer[icol*8 + 3] = ((display_buffer_show[y_index][x_index][icol] & 0b00010000)>0 ? PWM_ONE : PWM_ZERO ); |
14 | dma_buffer[icol*8 + 4] = ((display_buffer_show[y_index][x_index][icol] & 0b00001000)>0 ? PWM_ONE : PWM_ZERO ); |
15 | dma_buffer[icol*8 + 5] = ((display_buffer_show[y_index][x_index][icol] & 0b00000100)>0 ? PWM_ONE : PWM_ZERO ); |
16 | dma_buffer[icol*8 + 6] = ((display_buffer_show[y_index][x_index][icol] & 0b00000010)>0 ? PWM_ONE : PWM_ZERO ); |
17 | dma_buffer[icol*8 + 7] = ((display_buffer_show[y_index][x_index][icol] & 0b00000001)>0 ? PWM_ONE : PWM_ZERO ); |
18 | }
|
19 | // adjust x index according to strip arrangement
|
20 | // HERE: 0/0 lower left, alternating x direction with each line
|
21 | if(y_index%2 == 0) |
22 | {
|
23 | // check for display sizes
|
24 | if(x_index == DISPX-1) // Border reached |
25 | {
|
26 | // next y line
|
27 | y_index++; |
28 | // No need to change x_index, as it is decreasing in next line
|
29 | }
|
30 | else
|
31 | {
|
32 | x_index++; |
33 | }
|
34 | |
35 | }
|
36 | else
|
37 | {
|
38 | // Odd line
|
39 | // check for display sizes
|
40 | if(x_index == 0) // Border reached |
41 | {
|
42 | // next y line
|
43 | y_index++; |
44 | // No need to change x_index, as it is decreasing in next line
|
45 | }
|
46 | else
|
47 | {
|
48 | x_index--; |
49 | }
|
50 | }
|
51 | // End of frame update
|
52 | }
|
53 | else if(res_index < res_th ) // 0-based index! |
54 | {
|
55 | if(res_index <2) |
56 | {
|
57 | // for the first two reset frame calls, fill lower buffer with 0
|
58 | for(int i = 0; i<DMABUFSIZE/2; i++) |
59 | {
|
60 | dma_buffer[i] = 0; |
61 | }
|
62 | }
|
63 | res_index++; |
64 | }
|
65 | else if(res_index >= res_th) |
66 | {
|
67 | // new frame starts
|
68 | x_index = 0; |
69 | y_index = 0; |
70 | res_index = 0; |
71 | // load first frame into lower dma_buffer
|
72 | // for each bit, the correspondig pwm value is set
|
73 | for (uint8_t icol = 0; icol<COLORS; icol++) |
74 | {
|
75 | // test each bit
|
76 | dma_buffer[icol*8 + 0] = ((display_buffer_show[y_index][x_index][icol] & 0b10000000)>0 ? PWM_ONE : PWM_ZERO ); |
77 | dma_buffer[icol*8 + 1] = ((display_buffer_show[y_index][x_index][icol] & 0b01000000)>0 ? PWM_ONE : PWM_ZERO ); |
78 | dma_buffer[icol*8 + 2] = ((display_buffer_show[y_index][x_index][icol] & 0b00100000)>0 ? PWM_ONE : PWM_ZERO ); |
79 | dma_buffer[icol*8 + 3] = ((display_buffer_show[y_index][x_index][icol] & 0b00010000)>0 ? PWM_ONE : PWM_ZERO ); |
80 | dma_buffer[icol*8 + 4] = ((display_buffer_show[y_index][x_index][icol] & 0b00001000)>0 ? PWM_ONE : PWM_ZERO ); |
81 | dma_buffer[icol*8 + 5] = ((display_buffer_show[y_index][x_index][icol] & 0b00000100)>0 ? PWM_ONE : PWM_ZERO ); |
82 | dma_buffer[icol*8 + 6] = ((display_buffer_show[y_index][x_index][icol] & 0b00000010)>0 ? PWM_ONE : PWM_ZERO ); |
83 | dma_buffer[icol*8 + 7] = ((display_buffer_show[y_index][x_index][icol] & 0b00000001)>0 ? PWM_ONE : PWM_ZERO ); |
84 | }
|
85 | // increment x index, as first dataset is in dma buffer
|
86 | x_index++; |
87 | }
|
88 | }
|
89 | |
90 | //void dma_callback_complete(DMA_HandleTypeDef *_hdma)
|
91 | void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) |
92 | {
|
93 | if(x_index < DISPX && y_index < DISPY) // 0-based index! |
94 | {
|
95 | // load frame into upper dma_buffer
|
96 | // for each bit, the correspondig pwm value is set
|
97 | for (uint8_t icol = 0; icol<COLORS; icol++) |
98 | {
|
99 | // test each bit
|
100 | dma_buffer[DMABUFSIZE/2 + icol*8 + 0] = ((display_buffer_show[y_index][x_index][icol] & 0b10000000)>0 ? PWM_ONE : PWM_ZERO ); |
101 | dma_buffer[DMABUFSIZE/2 + icol*8 + 1] = ((display_buffer_show[y_index][x_index][icol] & 0b01000000)>0 ? PWM_ONE : PWM_ZERO ); |
102 | dma_buffer[DMABUFSIZE/2 + icol*8 + 2] = ((display_buffer_show[y_index][x_index][icol] & 0b00100000)>0 ? PWM_ONE : PWM_ZERO ); |
103 | dma_buffer[DMABUFSIZE/2 + icol*8 + 3] = ((display_buffer_show[y_index][x_index][icol] & 0b00010000)>0 ? PWM_ONE : PWM_ZERO ); |
104 | dma_buffer[DMABUFSIZE/2 + icol*8 + 4] = ((display_buffer_show[y_index][x_index][icol] & 0b00001000)>0 ? PWM_ONE : PWM_ZERO ); |
105 | dma_buffer[DMABUFSIZE/2 + icol*8 + 5] = ((display_buffer_show[y_index][x_index][icol] & 0b00000100)>0 ? PWM_ONE : PWM_ZERO ); |
106 | dma_buffer[DMABUFSIZE/2 + icol*8 + 6] = ((display_buffer_show[y_index][x_index][icol] & 0b00000010)>0 ? PWM_ONE : PWM_ZERO ); |
107 | dma_buffer[DMABUFSIZE/2 + icol*8 + 7] = ((display_buffer_show[y_index][x_index][icol] & 0b00000001)>0 ? PWM_ONE : PWM_ZERO ); |
108 | }
|
109 | // adjust x index according to strip arrangement
|
110 | // HERE: 0/0 lower left, alternating x direction with each line
|
111 | if(y_index%2 == 0) |
112 | {
|
113 | // check for display sizes
|
114 | if(x_index == DISPX-1) // Border reached |
115 | {
|
116 | // next y line
|
117 | y_index++; |
118 | // No need to change x_index, as it is decreasing in next line
|
119 | }
|
120 | else
|
121 | {
|
122 | x_index++; |
123 | }
|
124 | |
125 | }
|
126 | else
|
127 | {
|
128 | // Odd line
|
129 | // check for display sizes
|
130 | if(x_index == 0) // Border reached |
131 | {
|
132 | // next y line
|
133 | y_index++; |
134 | // No need to change x_index, as it is decreasing in next line
|
135 | }
|
136 | else
|
137 | {
|
138 | x_index--; |
139 | }
|
140 | }
|
141 | // End of frame update
|
142 | }
|
143 | else if(res_index < res_th ) // 0-based index! |
144 | {
|
145 | if(res_index <2) |
146 | {
|
147 | // for the first two reset frame calls, fill upper buffer with 0
|
148 | for(int i = 0; i<DMABUFSIZE/2; i++) |
149 | {
|
150 | dma_buffer[DMABUFSIZE/2 + i] = 0; |
151 | }
|
152 | }
|
153 | res_index++; |
154 | }
|
155 | else if(res_index >= res_th) |
156 | {
|
157 | // new frame starts
|
158 | x_index = 0; |
159 | y_index = 0; |
160 | res_index = 0; |
161 | // load first frame into upper dma_buffer
|
162 | // for each bit, the correspondig pwm value is set
|
163 | for (uint8_t icol = 0; icol<COLORS; icol++) |
164 | {
|
165 | // test each bit
|
166 | dma_buffer[DMABUFSIZE/2 + icol*8 + 0] = ((display_buffer_show[y_index][x_index][icol] & 0b10000000)>0 ? PWM_ONE : PWM_ZERO ); |
167 | dma_buffer[DMABUFSIZE/2 + icol*8 + 1] = ((display_buffer_show[y_index][x_index][icol] & 0b01000000)>0 ? PWM_ONE : PWM_ZERO ); |
168 | dma_buffer[DMABUFSIZE/2 + icol*8 + 2] = ((display_buffer_show[y_index][x_index][icol] & 0b00100000)>0 ? PWM_ONE : PWM_ZERO ); |
169 | dma_buffer[DMABUFSIZE/2 + icol*8 + 3] = ((display_buffer_show[y_index][x_index][icol] & 0b00010000)>0 ? PWM_ONE : PWM_ZERO ); |
170 | dma_buffer[DMABUFSIZE/2 + icol*8 + 4] = ((display_buffer_show[y_index][x_index][icol] & 0b00001000)>0 ? PWM_ONE : PWM_ZERO ); |
171 | dma_buffer[DMABUFSIZE/2 + icol*8 + 5] = ((display_buffer_show[y_index][x_index][icol] & 0b00000100)>0 ? PWM_ONE : PWM_ZERO ); |
172 | dma_buffer[DMABUFSIZE/2 + icol*8 + 6] = ((display_buffer_show[y_index][x_index][icol] & 0b00000010)>0 ? PWM_ONE : PWM_ZERO ); |
173 | dma_buffer[DMABUFSIZE/2 + icol*8 + 7] = ((display_buffer_show[y_index][x_index][icol] & 0b00000001)>0 ? PWM_ONE : PWM_ZERO ); |
174 | }
|
175 | // increment x index, as first dataset is in dma buffer
|
176 | x_index++; |
177 | }
|
178 | }
|
179 | void fill_area(int16_t xorigin, int16_t yorigin, uint8_t xend, uint8_t yend, uint8_t red, uint8_t green, uint8_t blue, uint8_t dpbuf[DISPY][DISPX][COLORS]) |
180 | {
|
181 | uint8_t xtemp = xorigin; |
182 | while(xtemp <= xend && xtemp < DISPX) |
183 | {
|
184 | uint8_t ytemp = yorigin; |
185 | while(ytemp <= yend && ytemp < DISPY) |
186 | {
|
187 | if(xtemp >= 0 && ytemp >= 0) |
188 | {
|
189 | dpbuf[ytemp][xtemp][RED] = red; |
190 | dpbuf[ytemp][xtemp][GREEN] = green; |
191 | dpbuf[ytemp][xtemp][BLUE] = blue; |
192 | }
|
193 | |
194 | ytemp++; |
195 | }
|
196 | xtemp++; |
197 | }
|
198 | }
|
Diese Routinen müssen beim Programmstart als Interrupt-Callbacks registriert werden:
1 | // Register DMA Callbacks
|
2 | // Half Complete
|
3 | HAL_DMA_RegisterCallback(&hdma_tim3_ch1_trig, HAL_DMA_XFER_HALFCPLT_CB_ID, dma_callback_halfcomplete); |
4 | // Complete is handled by void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) !!!
|
5 | // DMA Start
|
6 | HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_1, dma_buffer, DMABUFSIZE); |
7 | // Frame updates will be handled automatically by DMA IRQ
|
8 | // 2 Dataframes delay, as buffer has to be filled by dma handler
|
Zusätzlich muss - bei doppeltem Puffer - dieser natürlich regelmäßig upgedatet werden:
1 | memcpy(display_buffer_show, display_buffer_dim, sizeof(display_buffer)); |
Die Konfiguration ist dabei wie folgt (Insbesondere die für den Timer und Timer-Clock ist wichtig): MCU STM32F407VGTx HCLK = 160 MHz APB1 Timer Clock = 80 MHz APB2 Timer clock = 160 MHz PWM Generation CH1 / TIM3 Prescaler (PSC - 16 bits value) 0 Counter Mode Up Counter Period (AutoReload Register - 16 bits value ) 100 * Internal Clock Division (CKD) No Division Master/Slave Mode Disable (no sync between this TIM (Master) and its Slaves Trigger Event Selection Reset (UG bit from TIMx_EGR) PWM Generation Channel 1: Mode PWM mode 1 Pulse (16 bits value) 0 Fast Mode Enable * CH Polarity High DMA request Stream Direction Priority TIM3_CH1/TRIG DMA1_Stream4 Memory To Peripheral Very High * TIM3_CH1/TRIG: DMA1_Stream4 DMA request Settings: Mode: Circular * Use fifo: Disable Peripheral Increment: Disable Memory Increment: Enable * Peripheral Data Width: Half Word Memory Data Width: Half Word Ich hoffe, du (und weitere :) kannst damit was anfangen. Gruß Daniel
Ich habe nun versucht die Dateien von Frank für den STM32F103 auf HAL umzustellen (siehe Anhang). Die Erkennung des externen PullUps funktioniert und es wird der Pin auf OpenDrain gestellt. Die Timer und DMA Strukturen sind bei HAL anders aufgebaut und werden anders verknüpft und gestartet. Ich habe mich dabei an der schon genannten Bibliothek von Markus orientiert. Er arbeitet zwar nach einem ähnlichen Prinzip, benutzt aber 3 DMA Kanäle. Ich vermute mal für die parallele Ansteuerung mehrerer Stripes, die er dann im Wechsel befüllt. Frank schreibt glaube ich direkt per DMA in das Timer Compare Register und nutzt nur den DMA Interrupt. Hier wird dann per HalbTransfer und VollTransfer Callback die Umschaltung im Buffer gemacht. Bei mir kommt leider gar nichts am PWM Pin raus und das Programm hängt in ws2812_refresh() in der while(ws2812_dma_status != 0). Da der DutyCyle nur per DMA geändert wird, passt also irgendwas an meiner Timer und/oder DMA Konfiguration nicht. Bin gerade etwas ratlos, wie ich da weiter vorgehen soll. Vielleicht sieht auch jemand einen Fehler in der Konfiguration?
Ich muss wohl nach der PullUp Erkennung den Pin auf "GPIO_MODE_AF_OD" stellen, dann kommt mit einem festen Testwert für TIM1->CCR1 und ohne Aufruf von ws2812_dma_init() und weiterer WS2812 Funktionen immerhin eine feste PWM raus. Also scheint die DMA Konfiguration noch fehlerhaft zu sein. Vermutlich das Verlinken von DMA und Timer und/oder das Starten von DMA. Kann hier bitte mal jemand drauf schauen.
Kann mir jemand erklären, wie ich mit HAL_TIM_PWM_Start_DMA(&WS2812_TimHandle, TIM_CHANNEL_1, (uint32_t*)dma_buf, DMA_BUF_LEN); den DMA Halbtransfer und TransferComplete Interrupt realisieren kann? Es scheint kein separater DMA_HandleTypeDef nötig zu sein. Es werden automatisch ein paar Callbacks registriert. Aber kann ich dann auch das Doublebuffering mit Circular Mode und Half und Complete Interrupts nutzen? Oder kann man mit HAL auch noch einen Timer und einen DMA_HandleTypeDef verknüpfen?
@Frank: Wie wird denn bei deiner Variante nach jeder Timerperiode (sprich jedem Bit) ein neuer Wert an den Timer übergeben? Das soll ja der DMA machen, aber wie bekommt er mit, dass die Timerperiode um ist? Einen Interrupt dafür sehe ich nicht, das muss also irgendwie implizit passieren. Ich glaube diese Stelle klappt bei mir mit HAL nicht. Der Timer startet und gibt mittlerweile fortlaufend "Nullen" aus (das Timing stimmt, 1,25us Periode mit 35% DC) das Pogramm hängt aber in ws2812_refresh() in der while Schleife und wartet auf das Ende des DMA Transfers.
Dafür wird der "Half-Transfer-Interrupt" verwendet:
1 | # define WS2812_DMA_CHANNEL_IRQ_TC DMA1_IT_TC2 // transfer complete interrupt
|
2 | # define WS2812_DMA_CHANNEL_IRQ_HT DMA1_IT_HT2 // half-transfer interrupt
|
1 | void
|
2 | WS2812_DMA_CHANNEL_ISR (void) |
3 | {
|
4 | #if defined (STM32F4XX)
|
5 | if (DMA_GetITStatus(WS2812_DMA_STREAM, WS2812_DMA_CHANNEL_IRQ_HT)) // check half-transfer interrupt flag |
6 | {
|
7 | DMA_ClearITPendingBit (WS2812_DMA_STREAM, WS2812_DMA_CHANNEL_IRQ_HT); // reset flag |
8 | ws2812_setup_dma_buf (0); |
9 | }
|
10 | if (DMA_GetITStatus(WS2812_DMA_STREAM, WS2812_DMA_CHANNEL_IRQ_TC)) // check transfer complete interrupt flag |
11 | {
|
12 | DMA_ClearITPendingBit (WS2812_DMA_STREAM, WS2812_DMA_CHANNEL_IRQ_TC); // reset flag |
13 | |
14 | if (current_dma_buf_pos < current_data_pause_len) |
15 | {
|
16 | ws2812_setup_dma_buf (1); |
17 | }
|
18 | else
|
19 | {
|
20 | DMA_Cmd (WS2812_DMA_STREAM, DISABLE); // disable DMA |
21 | ws2812_dma_status = 0; // set status to ready |
22 | }
|
23 | }
|
24 | #elif defined (STM32F10X)
|
25 | if (DMA_GetITStatus(WS2812_DMA_CHANNEL_IRQ_HT)) // check half-transfer interrupt flag |
26 | {
|
27 | DMA_ClearITPendingBit (WS2812_DMA_CHANNEL_IRQ_HT); // reset flag |
28 | ws2812_setup_dma_buf (0); |
29 | }
|
30 | if (DMA_GetITStatus(WS2812_DMA_CHANNEL_IRQ_TC)) // check transfer complete interrupt flag |
31 | {
|
32 | DMA_ClearITPendingBit (WS2812_DMA_CHANNEL_IRQ_TC); // reset flag |
33 | |
34 | if (current_dma_buf_pos < current_data_pause_len) |
35 | {
|
36 | ws2812_setup_dma_buf (1); |
37 | }
|
38 | else
|
39 | {
|
40 | DMA_Cmd (WS2812_DMA_STREAM, DISABLE); // disable DMA |
41 | ws2812_dma_status = 0; // set status to ready |
42 | }
|
43 | }
|
44 | #endif
|
45 | }
|
In ws2812_dma_start() wird dieser dann eingeschaltet:
1 | DMA_ITConfig(WS2812_DMA_STREAM, DMA_IT_TC | DMA_IT_HT, ENABLE); // enable transfer complete and half transfer interrupt |
DMA_IT_TC: Transfer Complete Interrupt DMA_IT_HT: Half Transfer Interrupt
Nachtrag: Es wird hier ein zirkulärer DMA-Buffer verwendet, der immer nach der Hälfte eines DMA-Transfers nachgeladen wird. Du musst Dir das so vorstellen wie eine Uhr mit nur einem Sekundenzeiger: Immer, wenn der Sekundenzeiger unten ist (nach der ersten halben Umdrehung), werden die Daten für die übernächste(!) Hälfte nachgeladen. Immer, wenn der Sekundenzeiger oben ist (zweite halbe Umdrehung), werden die Daten für die übernächste(!) Hälfte nachgeladen. Es ist immer die übernächste Hälfte, die nachgeladen wird, da ja die nächste Hälfte bereits im DMA-Buffer liegt. So kann das Programm ganz bequem und ohne Zeitnot nachladen. Insgesamt sind im DMA-Buffer lediglich die Daten für 2 LEDs drin (48 Bit). Dann läuft das so:
1 | 1. Füllen Daten für LED 1 und LED 2 |
2 | 2. HT-Interrupt (nach Übertragung LED 1): Füllen Daten für LED 3 |
3 | 3. TC-Interrupt (nach Übertragung LED 2): Füllen Daten für LED 4 |
4 | Allgemein: |
5 | 4. HT-Interrupt (nach Übertragung LED n): Füllen Daten für LED n + 2 |
6 | 5. TC-Interrupt (nach Übertragung LED m): Füllen Daten für LED m + 2 |
7 | 6. Weiter bei 4. |
Im DMA-Buffer sind bei Auftreten eines Interrupts immer noch die Daten für die LED n + 1 drin. Du füllst dann die Daten für LED n + 2 nach. Der Vorteil ist: Der DMA-Buffer muss nur die Daten für 2 LEDs halten können. Bei den klassischen DMA-Transfers brauchst Du bei vielen LEDs (zum Beispiel 200) auch einen ebensolchen großen Buffer. Da sind dann schnell schon mal 10KB RAM und mehr belegt. Die ganze Prozedur - wie oben bechrieben - ist zwar ein wenig komplizierter, aber wenn man es einmal verstanden und korrekt umgesetzt hat, macht es richtig Spaß.
:
Bearbeitet durch Moderator
Hallo Frank, danke für die Erklärung. Eine Timerperiode entspricht doch einem Bit, richtig? Die 24 Byte für eine LED (also der halbe DMA Buffer) enthalten die 24 Timerwerte die für eine LED rausgeschoben werden. Muss dann nicht noch nach jeder Timerperiode nachgeladen werden, oder macht das der DMA automatisch? Ich habe auch libs gesehen, wo noch Tuner Interrupts dafür verwendet werden. Diesen Teil habe ich bei dir nicht verstanden.
Bernd schrieb: > Muss dann nicht noch nach jeder Timerperiode nachgeladen werden, oder > macht das der DMA automatisch? Das macht der DMA automatisch unter Zuhilfenahme der WS2812_DMA_CHANNEL_ISR(). Sind die ersten 24 Bits (0-23) raus, kommt der HT-Interrupt und füllt die Bits 48-71 nach. Sind dann die zweiten 24 Bits (24-47) raus, kommt dann der TC-Interrupt und füllt die Bits 72-95 nach. Und so weiter... Wenn ich in Deinen WS2812-Source reinschaue, hast Du das aber alles auskommentiert. Kann ja gar nicht mehr gehen.
:
Bearbeitet durch Moderator
Frank M. schrieb: > Wenn ich in Deinen WS2812-Source reinschaue, hast Du das aber alles > auskommentiert. Kann ja gar nicht mehr gehen. Ich habe es in zwei Callback Funktionen ausgelagert. WS2812_DMA_TransferHalfHandler() und WS2812_DMA_TransferCompleteHandler() die dann durch HAL_DMA_IRQHandler(&WS2812_DMAHandle) in WS2812_DMA_CHANNEL_ISR() aufgerufen werden. Ich habe es leider nicht zum Laufen bekommen und mein kleines Projekt von HAL auf StdPeriphLib umgestellt. Jetzt läuft es mit deinem Originalcode. Danke dafür!
Bernd schrieb: > Ich habe es leider nicht zum Laufen bekommen und mein kleines Projekt > von HAL auf StdPeriphLib umgestellt. Das war wahrscheinlich das sinnvollste. > Jetzt läuft es mit deinem Originalcode. Freut mich, gratuliere :-)
Frank M. schrieb: > Der Vorteil ist: Der DMA-Buffer muss nur die Daten für 2 LEDs halten > können. Es ist diskutabel, ob das unbedingt ein Vorteil ist. Warum rechnet niemand die dauernde Herumspringerei und die dafür benötigte (verlorene) Zeit als Nachteil ? > Bei den klassischen DMA-Transfers brauchst Du bei vielen LEDs > (zum Beispiel 200) auch einen ebensolchen großen Buffer. Da sind dann > schnell schon mal 10KB RAM und mehr belegt. Nein, bestimmt nicht. Ich mache es mit SPI und zwar mit 4 DMA-bit für 1 RGB-bit weil es so viel schneller ist als mit 3 DMA-bit für 1 RGB-bit. Bei 200LEDs ergibt das 2400Byt für SPI-Array. Die 600Byt für RGB-Array hat man sowieso - ob mit Timer oder SPI. Insgesamt sind das 3000Byt - woher du die 10KB her hast, ist für mich schleierhaft. Dafür aber die unbestreitbaren Vorteile: Fire and Forget - 99.999% der Zeit braucht sich der Prozessor nicht um DMA-Transfer zu den LEDs zu kümmern. Die ganze Prozedur ist viel einfacher und schneller.
Marc V. schrieb: > Warum rechnet niemand die dauernde Herumspringerei und die dafür > benötigte (verlorene) Zeit als Nachteil ? Weil diese benötigte Zeit verschwindend gering ist. Auf der WordClock24 laufen Animationen mit über 400 LEDs absolut flüssig. Rechne doch mal aus, was das kostet, das kann man vergessen. Marc V. schrieb: > Ich mache es mit SPI und zwar mit 4 DMA-bit für 1 RGB-bit weil es > so viel schneller ist als mit 3 DMA-bit für 1 RGB-bit. Schön, wo kann man sich Deinen Code anschauen? > Bei 200LEDs ergibt das 2400Byt für SPI-Array. Es gibt durchaus Anwendungen mit mehr als 200 LEDs. > Die 600Byt für RGB-Array hat man sowieso - ob mit Timer oder SPI. Ja. > Insgesamt sind das 3000Byt - woher du die 10KB her hast, ist für > mich schleierhaft. Hast Du da nicht die notwendigen Bytes für die 50usec Pause nach einem Frame vergessen? Oder wie machst Du das? Da kommt nämlich auch noch etwas zusammen - gerade wenn man die neueren WS2813 nimmt. Die benötigen sogar 280usec Pause zwischen den Frames. Aber okay, vernachlässigen wir das mal. Rechnen wir doch mal: Sei N die Anzahl der LEDs, dann ist der RAM-Verbrauch nach Deiner Methode: RAM = 3 x N + 12 x N = 15 x N Beispiele: 200 LEDs: 3000 Bytes 400 LEDs: 6000 Bytes 800 LEDs: 12000 Bytes Nach meiner Methode: RAM = 3 x N + 48 Beispiele: 200 LEDs: 648 Bytes 400 LEDs: 1248 Bytes 800 LEDs: 2400 Bytes Meine Methode benötigt also rund ein Fünftel gegenüber Deiner Methode. Bei der WC24, die etwas mehr als 400 LEDs ansteuert, liegt die Ersparnis bei knapp 5KB RAM. Bei einem STM32F103C8T6 mit lediglich 20KB RAM, der beim WordClock-Projekt eingesetzt wird, kann dieser Speicher durchaus sinnvoller eingesetzt werden. Manche Erweiterungen in den letzten Jahren wären ohne die obigen Optimierungen gar nicht mehr möglich gewesen. > Fire and Forget- 99.999% der Zeit braucht sich der Prozessor nicht > um DMA-Transfer zu den LEDs zu kümmern. Das gilt auch noch, wenn man die obige Optimierung einbaut, dann sinds halt 99,8 Prozentz. Ich habe es jedenfalls nicht geschafft, die WS2812-Transfers durch irgendwelche gleichzeitig ablaufenden Aktionen kaputtzukriegen. > Die ganze Prozedur ist viel einfacher und schneller. Einfacher: ja Schneller: minimal, vernachlässigbar RAM: erheblich schlechter, s.o. Es kommt halt immer auf die vorhandenen Ressourcen an.
:
Bearbeitet durch Moderator
Marc V. schrieb: > Warum rechnet niemand die dauernde Herumspringerei und die dafür > benötigte (verlorene) Zeit als Nachteil ? Nach längerem Nachdenken komme ich sogar zu dem Schluss, dass der Trick mit dem zirkulären DMA-Buffer sogar reale Zeit spart. Und zwar aus folgendem Grund: Müssen die kompletten 2400 Bytes vor der Anzeige in den DMA-Buffer geschoben werden, benötigt man dafür eine Zeit T, bevor überhaupt etwas zu sehen ist. Um den lediglich 48 Byte großen zirkulären Buffer zu füllen, braucht man nur ein fünfzigstel der Zeit, also T/50. Die andere Zeit wird erst während der Übertragung beansprucht, benötigt also keine zusätzliche Real-Time mehr. Die Anzeige erfolgt also eher und ist genauso schnell. Das zusätzlich verwendete Double-Buffering wurde hier noch gar nicht betrachtet. Hier kann während einer DMA-Übertragung schon der nächste Frame komplett ausgerechnet und auch schon der zweite 48 Byte große Buffer vorbereitet werden, so dass hier ohne irgendwelche großen Denkpausen ununterbrochen gefeuert werden kann.
Frank M. schrieb: > Marc V. schrieb: >> Ich mache es mit SPI und zwar mit 4 DMA-bit für 1 RGB-bit weil es >> so viel schneller ist als mit 3 DMA-bit für 1 RGB-bit. > > Schön, wo kann man sich Deinen Code anschauen? Wozu ? Das ist ganz normaler Code - wenn bit in RGB_Arr[x] == 0, dann ist Nibble in SPI_Arr[y] = 0xE0 / 0x0E, else SPI_Arr[y] = 0x80 / 0x08. > Hast Du da nicht die notwendigen Bytes für die 50usec Pause nach einem > Frame vergessen? Oder wie machst Du das? Da kommt nämlich auch noch Was für Bytes meinst du ? Das verstehe ich nun wirklich nicht.
Marc V. schrieb: > Wozu Du meinst also, dem TO bei der Portierung eines Moduls helfen zu können, wenn Du ein wenig Prosa schreibst: "Ich mache das so: ..." und sonst ausschließlich an einer konkreten Lösung, die zudem mittlerweile beim TO erfolgreich läuft, auch noch herummäkelst. Sorry, kann ich nicht nachvollziehen. Wenn man sogar noch in höchsten Tönen prahlt mit einem Source, den man gar nicht vorzeigen will, dann ist das für mich indiskutabel. Wir können uns auch gerne über physikalische Phänomene in schwarzen Löchern unterhalten. Ist genauso wenig zielführend. Marc V. schrieb: > Was für Bytes meinst du ? Die Null-Bytes, mit denen die meisten im Netz herumschwirrenden WS2812-DMA-Routinen garantieren, dass nach der Übertragung 50usec lang Ruhe auf der Leitung ist. Für mich ist hier EOD.
:
Bearbeitet durch Moderator
Frank M. schrieb: > Du meinst also, dem TO bei der Portierung eines Moduls helfen zu können, > wenn Du ein wenig Prosa schreibst: "Ich mache das so: ..." und sonst > ausschließlich an einer konkreten Lösung, die zudem mittlerweile beim TO > erfolgreich läuft, auch noch herummäkelst. Wer mäkelt herum?
1 | > Der Vorteil ist: Der DMA-Buffer muss nur die Daten für 2 LEDs halten |
2 | > können. |
Ich war ganz einfach nicht mit obiger Aussage von dir einverstanden. Und wieso dies herummäkeln "an einer konkreten Lösung, die zudem mittlerweile beim TO erfolgreich läuft" sein soll, ist wiederum eine Aussage von dir, dessen Logik sich mir völlig entzieht. Frank M. schrieb: > Wenn man sogar noch in höchsten Tönen prahlt mit einem Source, den man > gar nicht vorzeigen will, dann ist das für mich indiskutabel. Wer prahlt wo mit was ? Ich habe weder dich noch dein Program schlechtgemacht oder angegriffen, noch habe ich mit irgendeinem Source geprahlt, wozu solche Töne ? Dein Benehmen könnte man als komplexbeladen bezeichnen, aber ich will das natürlich nicht tun. > Marc V. schrieb: >> Was für Bytes meinst du ? > > Die Null-Bytes, mit denen die meisten im Netz herumschwirrenden > WS2812-DMA-Routinen garantieren, dass nach der Übertragung 50usec lang > Ruhe auf der Leitung ist. Ich benutze dafür ein Timer mit Flag. Auch Null-Bytes garantieren natürlich nicht, dass auf der Leitung Ruhe ist. Aber selbst wenn: Bei 800KHz braucht man für 50us 40bit oder 5Bytes. Für 300us (WS2813) braucht man 30Bytes. Wie und mit welcher Mathematik du aber trotzdem auf die Differenz von 7000Bytes kommst, ist mir wiederum unklar. > Für mich ist hier EOD. Eine Diskussion setzt Dialog voraus, bei dir ist es überwiegend ein Monolog, deswegen solltest du EOM schreiben.
:
Bearbeitet durch User
Frank M. schrieb: > Sei N die Anzahl der LEDs, dann ist der RAM-Verbrauch nach Deiner > Methode: > > RAM = 3 x N + 12 x N = 15 x N > > Beispiele: > > 200 LEDs: 3000 Bytes > 400 LEDs: 6000 Bytes > 800 LEDs: 12000 Bytes > > Nach meiner Methode: > > RAM = 3 x N + 48 > > Beispiele: > > 200 LEDs: 648 Bytes > 400 LEDs: 1248 Bytes > 800 LEDs: 2400 Bytes Was ist wenn man von beiden Varianten eine "bessere" macht?! Auch beim SPI kann man HT, TC nutzen. Bei 200leds alle 33khz in die ISR zu hüpfen ist auch ein bissschen overhead. Jeder hat einen gewissen Einsetzzweck, da kann es schon bei einen oder fire and forget reichen. Ich sehe bei SPI nur Vorteile.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.