Gadający STM32 - Zastosowanie kodeka Speex.pdf

(928 KB) Pobierz
080-083_stm.indd
PODZESPOŁY
Gadający STM32
Zastosowanie kodeka Speex
do odtwarzania komunikatów
głosowych
Konstruktorzy urządzeń elektronicznych często decydują się na
dodanie do swoich projektów funkcji odtwarzania komunikatów
głosowych. Na łamach Elektroniki Praktycznej prezentowane
były już najróżniejsze „gadające” zegary, termometry czy też
woltomierze. Najczęściej zadanie rejestracji, przechowywania
i odtwarzania dźwięków powierzane było specjalizowanym
układom „magnetofonów” półprzewodnikowych, czyli układom
ISD14xx. Podyktowane to było stosunkowo prostą ich obsługą
a przede wszystkim brakiem innej tak prostej i stosunkowo taniej
technologii przechowywania dźwięku. Przechowywanie dźwięków
w „surowej” postaci jest ogromnie nieefektywne pod względem
wymaganej pojemności pamięci, natomiast dekompresja chociażby
najpopularniejszego formatu dźwięku MP3 wymaga bardzo dużej,
jak na typowe układy mikrokontrolerowe, mocy obliczeniowej, albo
specjalizowanych i trudno dostępnych dekoderów sprzętowych.
Przygotowanie nagrań
dźwiękowych
Komunikaty głosowe, które mają być odtwo-
rzone przez przykładową aplikację należy na-
grać na komputerze PC z wykorzystaniem do-
wolnego programu do rejestracji dźwięku, np.
systemowego „Rejestratora Dźwięku” i zapisać
je w formacie .wav . Ważne jest, aby częstotli-
wość próbkowania dźwięku podczas rejestracji
była równa 8 kHz. Rozdzielczość próbkowania
powinna wynosić 16-bitów. Następnie należy
dokonać kompresji nagrań za pomocą programu
speexenc.exe , który znajduje się w archiwum
z przykładową aplikacją przygotowaną przez
STMicroelectronics (AN2812). Program ten moż-
na również pobrać ze strony www.speex.org/
downloads . Program speexenc.exe jest aplikacją
pracującą w wierszu poleceń. W celu otrzyma-
nia pliku zakodowanego kodekiem Speex, który
będzie można odtworzyć za pomocą aplikacji
demonstracyjnej należy wywołać program spe-
exenc.exe z następującymi parametrami :
speexenc.exe -n --quality 4 input_
file output_file
Parametr input_file określa ścieżkę dostępu
do pliku z nagranym komunikatem dźwięko-
wym, natomiast parametr output_file określa
ścieżkę dostępu do pliku wyjściowego.
Aby możliwe było wykorzystanie tak otrzy-
manych danych w kodzie źródłowym, należy
dokonać konwersji pliku zawierającego dane
binarne do pliku tekstowego zawierającego ta-
blicę danych zawierającą liczbową reprezentację
każdego bajtu pliku binarnego. Do tego celu
można wykorzystać przykładowo program Hex
Workshop i za pomocą opcji Export wygene-
rować plik źródłowy języka C z tablicą zawie-
rającą dane z otwartego pliku. W omawianej
aplikacji, dane reprezentujące nagrane komuni-
katy znajdują się w pliku voice.h . Oprócz tego,
w pliku voice.h została zdefiniowana struktura
Sound_t . Definicjastrukturyjestprzedstawiona
następująca:
Jeszcze kilka lat temu zastosowanie we
własnym projekcie mikroprocesora 32-bito-
wego o wydajności kilkudziesięciu MIPS było
zadaniem będącym poza zasięgiem prze-
ciętnego amatora. Dziś w cenie zbliżonej do
najwyższych modeli wiodącej prym rodziny
mikrokontrolerów 8-bitowych można nabyć
bardzo szeroką gamę wydajnych 32-bitowych
mikrokontrolerów zbudowanych w oparciu
o rdzeń ARM, posiadających szeroką gamę
układów peryferyjnych. Niekwestionowanym
hitem ostatnich miesięcy są mikrokontrolery
STM32 wykorzystujące nowoczesny rdzeń Cor-
tex-M3. Producent tych mikrokontrolerów, fir-
ma STMicroelectronics, przygotował aplikację
(AN2812) demonstrującą możliwości kodeka
Speex w zakresie nagrywania i odtwarzania
komunikatów głosowych.
mach mikroprocesorowych, gdzie głównym
kryterium doboru oprogramowania jest niskie
zużycie pamięci programu oraz mocy oblicze-
niowej procesora.
Przykład zastosowania
W artykule zostanie przedstawiona aplikacja
wykorzystująca kodek Speex do odtwarzania
komunikatów głosowych przechowywanych
w pamięci Flash mikrokontrolera STM32. Apli-
kacja została przygotowana dla kompilatora
Raisonance, ale w prosty sposób może zostać
dostosowana do innych kompilatorów dla mi-
krokontrolerów STM32. Omawiany w artykule
program został przygotowany dla zestawu
ZL27ARM ( www.kamami.pl ) z mikrokontro-
lerem STM32F103VBT6 posiadającym 128 kB
pamięci Flash oraz 20 kB pamięci RAM, lecz
może zostać uruchomiony praktycznie na do-
wolnej platformie sprzętowej z mikrokontrole-
rem STM32. Zestaw powinien być wyposażony
w wyświetlacz LCD 2x16 znaków. Do zestawu
należy dołączyć prosty filtr RC, pokazany na
schemacie. Wyjście filtra należy podłączyć do
wzmacniacza mocy sterującego głośnikiem.
Możliwie jest podłączenie wyjścia PWM bez-
pośrednio z głośniczkiem znajdującym się na
płytce zestawu ZL27ARM, jednakże głośność
dźwięku będzie bardzo niska.
Czym jest kodek Speex?
Speex jest darmowym kodekiem o otwar-
tym kodzie źródłowym, zaprojektowanym
specjalnie dla przetwarzania mowy. Strona
WWW projektu jest dostępna pod adresem
www.speex.org . Kodek Speex został zapro-
jektowany z myślą o wykorzystaniu go w te-
lefonii internetowej VoIP, jednak ze względu
na stosunkowo niskie wymagania sprzętowe
idealnie nadaje się do wykorzystania w syste-
80
ELEKTRONIKA PRAKTYCZNA 12/2008
652389954.001.png
Zastosowanie kodeka Speex
typedef struct
{
u8 * Pointer;
u16 Length;
}Sounds_t;
Struktura ta składa się z dwóch pól: pierwsze
pole struktury jest wskaźnikiem do typu u8 i bę-
dzie przechowywać adres początku tablicy z za-
kodowanymi danymi, natomiast drugie pole
jest typu u16 i będzie przechowywać długość
komunikatu w ramkach. Długość komunikatu
w ramkach można obliczyć poprzez podziele-
nie rozmiaru pliku *.spx (czyli rozmiaru tablicy)
przez liczbę bajtów w ramce, czyli przez liczbę
20. Oprócz struktury w pliku voice.h została
zdefiniowana również tablica typu Sound_t
przechowująca dane wszystkich zapisanych
w pliku komunikatów dźwiękowych. Kod inicju-
jący tablicę przedstawiono niżej:
Sounds_t Sounds[3]={
{rawData1, 311},
{rawData2, 96},
{rawData3, 71},
};
W celu uzyskania danych określających kon-
kretny komunikat wystarczy odwołać się do wy-
branej pozycji tablicy, np.: Sounds[0].Poin-
ter zwróci adres komunikatu, a Sounds[0].
Length zwróci jego długość. Dzięki temu moż-
na w prosty sposób odtworzyć wszystkie komu-
nikaty za pomocą pętli iterującej po wszystkich
pozycjach tablicy, co zostanie wy-
korzystane w programie demon-
stracyjnym.
Dekodowanie dźwięku
z pamięci Flash
Ogólny algorytm odtwarza-
nia komunikatów dźwiękowych
przedstawiono na rys. 1 . Apli-
kacja wykorzystuje dwa bufory
do dekodowania i odtwarzania
nagrania. Dekodowanie dźwię-
ku zapisanego w pamięci Flash
mikrokontrolera dokonywane
jest w ramach funkcji PlaySo-
und . Funkcja ta przyjmuje dwa
parametry: wskaźnik do obsza-
ru pamięci, w którym znajdu-
je się zakodowany kodekiem
Speex dźwięk oraz jego długość
w ramkach. Każda ramka zako-
dowanego nagrania składa się
z dwudziestu bajtów, natomiast
ramka zdekodowanego nagra-
nia zajmuje 160 bajtów. Wynika
z tego, iż współczynnik kompre-
sji nagrania wynosi 1:8.
Kod funkcji PlaySound przedstawiono na
list. 1 .
Działanie funkcji rozpoczyna się od zdekodo-
wania dwóch pierwszych ramek nagrania i wy-
Rys. 1.
pełnienia obydwu buforów wyjściowych zdeko-
dowanymi danymi. Dekodowanie każdej ramki
nagrania przebiega w następujący sposób :
• tablica input_bytes musi zostać wypełnio-
na odczytaną z pamięci Flash ramką sygnału,
• następnie zawartość tablicy input_bytes
jest kopiowana, za pomocą funkcji speex_
bits_read_from , do struktury bits , stano-
wiącej „wejście” dekodera Speex,
• ramka znajdująca się w strukturze bits jest
dekodowana do bufora wyjściowego OutBuf-
fer , za pomocą funkcji speex_decode_int.
Po zdekodowaniu dwóch ramek i wypełnie-
niu obydwu buforów wyjściowych do zmiennej
Play zapisywana jest liczba 1. Zmienna Play
sygnalizuje fakt wypełnienia buforów zdekodo-
wanymi danymi i zezwala na ich odtworzenie
przez procedurę obsługi przerwania od timera
TIM2. Pozostała część nagrania jest dekodowa-
na w pętli, aż do osiągnięcia końca nagrania,
czyli do momentu, aż zmienna NB_Frames
zrówna się ze zmienną Length . Stan zmiennej
Start_Decoding określa, który z buforów
został opróżniony i powinien zostać wypeł-
niony nowymi danymi. Stan tej zmiennej jest
modyfikowany przez procedurę odtwarzania
dźwięku z buforów wyjściowych.
List. 1.
void PlaySound(const u8 *Sound, u16 Lenght)
{
vu16 i;
for(i=0;i<ENCODED_FRAME_SIZE; i++)
input_bytes[i] = *(Sound + sample_index++);
speex_bits_read_from(&bits, input_bytes, ENCODED_FRAME_SIZE);
speex_decode_int(dec_state, &bits, (spx_int16_t*)OUT_Buffer[0]);
for(i=0;i<ENCODED_FRAME_SIZE; i++)
input_bytes[i] = *(Sound + sample_index++);
speex_bits_read_from(&bits, input_bytes, ENCODED_FRAME_SIZE);
speex_decode_int(dec_state, &bits, (spx_int16_t*)OUT_Buffer[1]);
NB_Frames++;
Play = 1;
while(NB_Frames < Lenght)
{
if(Start_Decoding == 1)
{
for(i=0;i<ENCODED_FRAME_SIZE; i++)
input_bytes[i] = *(Sound + sample_index++);
speex_bits_read_from(&bits, input_bytes, ENCODED_FRAME_SIZE);
speex_decode_int(dec_state, &bits, (spx_int16_t*)OUT_Buffer[0]);
Start_Decoding = 0;
NB_Frames++;
}
if(Start_Decoding == 2)
{
for(i=0;i<ENCODED_FRAME_SIZE; i++)
input_bytes[i] = *(Sound + sample_index++);
speex_bits_read_from(&bits, input_bytes, ENCODED_FRAME_SIZE);
speex_decode_int(dec_state, &bits, (spx_int16_t*)OUT_Buffer[1]);
Start_Decoding = 0;
NB_Frames++;
}
}
Play = 0;
sample_index = 0;
NB_Frames = 0;
outBuffer = OUT_Buffer[0];
}
Odtwarzanie dźwięku z buforów
wyjściowych
Jako przetwornik cyfrowo-analogowy wyko-
rzystywany jest timer 1 pracujący w trybie PWM.
Współczynnik wypełnienia sygnału generowa-
nego przez timer 1 jest aktualizowany z często-
tliwością 8 kHz w ramach obsługi przerwania
od timera 2. Kod handlera przerwania od timera
TIM2 przedstawiono na list. 2 .
ELEKTRONIKA PRAKTYCZNA 12/2008
81
652389954.002.png
PODZESPOŁY
Na początku procedury następuje załadowa-
nie do rejestru ARR wartości, od której licznik
rozpoczyna odliczanie oraz wyzerowanie flagi
przerwania. Następnie sprawdzany jest stan
zmiennej Play , która określa czy dane z bufora
wyjściowego mają być przekazane na wyjście.
Jeżeli wartość zmiennej jest różna od zera, na-
stąpi załadowanie do rejestru CCR1 licznika
TIM1 wartości określającej aktualny współczyn-
nik wypełnienia generowanego sygnału PWM
oraz sprawdzenie, czy osiągnięto koniec które-
gokolwiek z buforów wyjściowych. W przypad-
ku, gdy cały aktualnie odtwarzany bufor został
odczytany, następuje przełączenie odtwarzania
na drugi z buforów. Jeśli natomiast bufor nie
został w pełni odtworzony, inkrementowany
jest wskaźnik outBuffer , który wskazuje na ak-
tualną pozycję w buforze wyjściowym. W przy-
padku, gdy zmienna Play jest wyzerowana, do
rejestru CCR1 timera TIM1 zapisywana jest stała
wartość 0x200, odpowiadająca w przybliżeniu
wartości „masy analogowej” na wyjściu sygna-
łu, czyli połowie napięcia maksymalnego gene-
rowanego przez układ PWM timera TIM1, gdyż
w przypadku omawianej aplikacji timery TIM1
i TIM2 pracują nieprzerwanie przez cały czas
działania aplikacji. Oryginalna aplikacja opraco-
wana przez firmę STMicroeletronics działa nieco
inaczej, gdyż timery są aktywowane tylko na
czas odtwarzania komunikatu, co objawia się
niestety nieprzyjemnymi trzaskami w momen-
cie włączania i wyłączania generatora PWM.
W przedstawianej w artykule aplikacji ten
problem nie występuje, kosztem niewielkiego
obciążenia procesora spowodowanego ciągłym
występowaniem przerwania od timera TIM2.
List. 2.
void TIM2_IRQHandler(void)
{
TIM2->ARR = TIM2ARRValue;
TIM2->SR = TIM_INT_Update;
if(Play)
{
TIM1->CCR1 = ((*outBuffer>>6)) + 0x200 ;
if(outBuffer == &OUT_Buffer[1][159])
{
outBuffer = OUT_Buffer[0];
Start_Decoding = 2;
}
else if(outBuffer == &OUT_Buffer[0][159])
{
outBuffer = OUT_Buffer[1];
Start_Decoding = 1;
}
else
{
outBuffer++;
}
}
else
{
TIM1->CCR1 = 0x200 ;
}
}
List. 3.
void Vocoder_Init(void)
{
/* Peripherals InitStructure define ----------------------------------------
-*/
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
/* TIM1 configuration ------------------------------------------------------
-*/
TIM_DeInit(TIM1);
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
GPIO_StructInit(&GPIO_InitStructure);
/* Configure PA.08 as alternate function (TIM1_OC1) */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA, GPIO_Pin_8);
/* TIM1 used for PWM genration */
TIM_TimeBaseStructure.TIM_Prescaler = 0x00; /* TIM1CLK = 72 MHz */
TIM_TimeBaseStructure.TIM_Period = 0x3FF; /* 10 bits resolution */
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
/* TIM1’s Channel1 in PWM1 mode */
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0x200;/* Duty cycle: 50%*/
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High;
TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set;
TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset;
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM1, ENABLE);
/* TIM2 configuration ------------------------------------------------------
-*/
TIM_DeInit(TIM2);
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
/* TIM2 used for timing, the timing period depends on the sample rate */
TIM_TimeBaseStructure.TIM_Prescaler = 0x00; /* TIM2CLK = 72 MHz */
TIM_TimeBaseStructure.TIM_Period = TIM2ARRValue;
TIM_TimeBaseStructure.TIM_ClockDivision = 0x0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
/* Output Compare Inactive Mode configuration: Channel1 */
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Inactive;
TIM_OCInitStructure.TIM_Pulse = 0x0;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Disable);
}
Inicjalizacja układów peryferyjnych
Ponieważ aplikacja wykorzystuje podczas pra-
cy dwa układy timerów konieczne jest ich wła-
ściwe zainicjalizowanie. Inicjalizacja wykorzy-
stywanych układów peryferyjnych realizowana
jest za pomocą funkcji Vocoder_Init , której kod
przedstawiono na list. 3 .
Timer TIM1 jest skonfigurowany do pracy
w trybie PWM. Wyjście PA8 jest skonfigurowane
jako wyjście funkcji alternatywnej, czyli w tym
przypadku wyjście PWM timera TIM1. Timer
TIM2 został skonfigurowany do generowania
przerwania z częstotliwością 8 kHz.
Konfiguracja układu przerwań
Ponieważ przesyłanie danych z buforów
wyjściowych na wyjście analogowe odbywa się
w ramach obsługi przerwania od timera TIM2,
konieczne jest odpowiednie skonfigurowanie
układu przerwań. Kod funkcji odpowiedzialnej
za konfigurację układu przerwań przedstawiono
na list. 4 .
Główny kod programu
Zasadnicza część aplikacji odtwarzającej ko-
munikaty z pamięci Flash mikrokontrolera jest
stosunkowo prosta, gdyż jej celem jest jedynie
praktyczne przedstawienie wykorzystania kodeka
Speex do odtwarzania uprzednio przygotowa-
nych komunikatów dźwiękowych. Kod głównej
funkcji programu przedstawiony jest na list. 5 .
82
ELEKTRONIKA PRAKTYCZNA 12/2008
652389954.003.png
Zastosowanie kodeka Speex
List. 4.
void InterruptConfig(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_DeInit();
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x00);
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQChannel;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
Działanie funkcji rozpoczyna się od inicjali-
zacji wykorzystywanych układów peryferyjnych
oraz kodeka. Następnie na wyświetlaczu LCD
wyświetlany jest ekran powitalny i po krótkiej
chwili oczekiwania następuje odtworzenie ko-
munikatów w pętli nieskończonej. Wykorzysta-
na do tego celu zostaje tablica Sounds prze-
chowująca adresy oraz długości poszczególnych
komunikatów. Oprócz odtworzenia komunikatu
na wyświetlaczu LCD jest wyświetlany numer ak-
tualnie odtwarzanego komunikatu.
List. 5.
int main(void)
{
vu8 i;
vu32 j;
Demo_Init(); //
LCD_Initialize(); //
Speex_Init(); // Inicjalizacja
Vocoder_Init(); //
Vocoder_Start(); //
LCD_GoTo(0,0); //
LCD_WriteText(„Speex Demo - EP”); //
LCD_GoTo(0,1); // Ekran powitalny
LCD_WriteText(„www.ep.com.pl”); //
for(j = 0; j < 0x3FFFFF; j++); // Opóźnienie
while(1) // pętla nieskończona
{
for(i = 0; i < 9; i++)
{
LCD_GoTo(0,1); //
LCD_WriteText(„Komunikat nr „); // Wyświetlenie napisu
LCD_WriteData(i+’0’); //
PlaySound(Sounds[i].Pointer, Sounds[i].Length); // Odtworzenie dźwięku
for(j = 0; j < 0xFFFFF; j++); // Opóźnienie
}
}
}
Podsumowanie
Przedstawiona w artykule aplikacja demon-
stracyjna zajmuje, łącznie z nagranymi komuni-
katami, około 30 kB pamięci Flash mikrokontro-
lera. Do dyspozycji programisty pozostaje jeszcze
spora ilość miejsca, w zależności od zastosowa-
nego typu mikrokontrolera, na zasadniczą apli-
kację, wykorzystującą kodek Speex do odtwarza-
nia dźwięku. Trudno bowiem sobie wyobrazić, iż
odtwarzanie komunikatów dźwiękowych może
być jedyną funkcją, do której zaprzęgnięty zo-
stanie mikrokontroler z rodziny STM32, stano-
wi to rzecz jasna tylko dodatek do szerokiego
grona aplikacji, w których zastosowanie mogą
znaleźć rewelacyjne mikrokontrolery STM32.
Radosław Kwiecień, EP
radoslaw.kwiecien@ep.com.pl
R
E
K
L
A
M
A
forum.ep.com.pl
ELEKTRONIKA PRAKTYCZNA 12/2008
83
652389954.004.png
Zgłoś jeśli naruszono regulamin