目標
- STM32F4のタイマを使って時分割多重パルス幅変調の信号を送信する
- シリアルバスなLEDを制御する
今回は7セグLEDモジュール(8LED)が2個と、WS2812Bが8個並んだスティック2本で、計32個のWS2812Bを制御します。
フルカラー7セグメントLED表示器 文字高1インチ: ディスプレイ・表示器 秋月電子通商-電子部品・ネット通販
マイコン内蔵RGB 8LEDスティック: LED(発光ダイオード) 秋月電子通商-電子部品・ネット通販
信号フォーマット
このLEDは3色各8bitで計24bitを送信し、それがカスケードされて24n個のパルスを送信することにより制御されます。
信号の同期は、一定時間パルスがなければシフトレジスタをクリアし、その次のパルスを最初のパルスとして認識します。
この信号はある程度のロバスト性があり、およそ645kbpsから1.05Mbpsの範囲で転送します。0を送信する時は短いパルスを、1を送信する時は長いパルスを使用し、2値パルス幅変調が行われます。
0の場合はHighが0.4us、Lowが0.85usで、1の場合はHighが0.8us、Lowが0.45usです。前述の通りある程度余裕があるので、あまりキッチリ仕上げる必要はありませんが、HighとLowのそれぞれで許容誤差が規定されているので注意してください。
パルスは上位ビットファーストで、最初の8bitが緑、次の8bitが赤、次の8bitが青、次の8bitは次のLEDの緑、という順番です。
手段
今回はSTM32F4のTIM3/ch1を使用して転送します。またパルス幅はの値はDMAで転送します。
Cubeの設定
TIM3のChannel1をPWM Generation CH1に設定します。
TIM3 ConfigrationでPrescalerは0、Periodは104に設定します。84MHzで動作しているので、84MHz/(104+1)=800kHzと、ちょうど標準の値になります。
PWM Generation Channel 1のModeはPWM mode 1、Pulseは0という設定にしておきます。
次にDMA Settingsタブを開き、DMAを追加してRequestをTIM3_CH1/TRIGに、DirectionをMemory To Peripheralに設定します。それ以外は初期設定です。
ソースコード
#include <cmsis_os.h>
#include <stm32f4xx.h>
#include <tim.h>
namespace
{
constexpr uint8_t number_of_pixels = 32;
uint16_t WS2812_pulse_width_data[24 * number_of_pixels + 1] = {};
constexpr uint8_t pulse_width_0 = 32; // 0.393usec @ 84MHz
constexpr uint8_t pulse_width_1 = 66; // 0.798usec @ 84MHz
} // namespace
bool set_pixel(uint8_t index, uint8_t red, uint8_t green, uint8_t blue)
{
bool flag = (index < number_of_pixels);
if (flag)
{
WS2812_pulse_width_data[index * 24 + 8 * 0 + 0] = (green & 0x80) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 0 + 1] = (green & 0x40) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 0 + 2] = (green & 0x20) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 0 + 3] = (green & 0x10) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 0 + 4] = (green & 0x08) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 0 + 5] = (green & 0x04) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 0 + 6] = (green & 0x02) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 0 + 7] = (green & 0x01) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 1 + 0] = (red & 0x80) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 1 + 1] = (red & 0x40) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 1 + 2] = (red & 0x20) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 1 + 3] = (red & 0x10) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 1 + 4] = (red & 0x08) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 1 + 5] = (red & 0x04) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 1 + 6] = (red & 0x02) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 1 + 7] = (red & 0x01) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 2 + 0] = (blue & 0x80) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 2 + 1] = (blue & 0x40) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 2 + 2] = (blue & 0x20) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 2 + 3] = (blue & 0x10) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 2 + 4] = (blue & 0x08) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 2 + 5] = (blue & 0x04) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 2 + 6] = (blue & 0x02) ? pulse_width_1 : pulse_width_0;
WS2812_pulse_width_data[index * 24 + 8 * 2 + 7] = (blue & 0x01) ? pulse_width_1 : pulse_width_0;
}
return (flag);
}
// FreeRTOS task
extern "C" void WS2812_driver_func(void const *argument)
{
for (uint8_t i = 0; i < number_of_pixels; ++i)
{
set_pixel(i, 0, 0, 0);
}
while (1)
{
HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_1, (uint32_t *)WS2812_pulse_width_data,
sizeof(WS2812_pulse_width_data) / sizeof(WS2812_pulse_width_data[0]));
osDelay(10);
HAL_TIM_PWM_Stop_DMA(&htim3, TIM_CHANNEL_1);
osDelay(20);
}
}
パルス幅は16bitで指定するので、予め16bit配列を確保しておきます。
set_pixel関数は受け取った引数に応じてこの配列の値を書き換えます。
転送開始はFreeRTOSのタスクで処理しますが、タスクが起動した後はset_pixelですべての値を初期化しておきます(0クリア配列ではパルス列が送信されません)。
今回は約33Hzでパルス列を送信しています。
LED32個の場合、約1msecで転送が終わるので、もう少し更新レートを上げられます。
DMA転送なので、CPUは転送中に別の作業を行えます。ただしデータバスはある程度使われているので、そのあたりは注意する必要があります。
結果
別のタスクからset_pixelを操作して色を指定しました。
かなり輝度を下げていますが、それでも写真に撮るとかなり明るいですね。
肉眼ではもっと色鮮やかで綺麗です。
おわりに
今回はTIMのPWMを1chだけ使いました。
同時に最大1chしか使わないならとても簡単にパルス幅変調を送信することが可能です。
ただ、複数ch(最大4ch)を同時に使おうとするとかなり面倒なので、そういう必要がある場合は覚悟を決めて一気にやってしまってください。