Minulý článek představil základní principy real-time operačního systému NuttX zaměřeného na vestavné systémy. Podívali jsme se na základní organizaci ovladačů dělených na společnou vyšší vrstvy a pro mikrokontrolér specifickou nižší vrstvu.
Co se dozvíte v článku
Analogově-digitální převodník (ADC)
Princip konfigurace této periferie je téměř stejný jako u blikání diodou. Rozdíl je v nižší vrstvě, která není implementována jako obecný kód, ale každý mikrokontrolér má svojí vlastní implementaci.
Z toho důvodu musíme v konfiguraci (zavolané přes make menuconfig či make qconfig) povolit periferii na dvou místech. Jednou v Systems Type → STM32L4 Peripheral Support, kde povolíme volbu ADC1 ( CONFIG_STM32L4_ADC1), podruhé v Analog Device (ADC/DAC) Support ( CONFIG_ANALOG) a podvolbu Analog to Digital Conversion ( CONFIG_ADC). První volbou jsme řekli, že chceme zakompilovat nižší vrstvu ovladače pro periferii ADC1. Mohli bychom samozřejmě zvolit jinou ADC periferie, pokud bychom chtěli převod na jiném pinu. Kód pro ADC periferie je v rámci jednoho mikrokontroléru stejný, separátní zapnutí jednotlivých periferií je z důvodu podmíněného kompilování definic a struktur pro danou periferii. NuttX se tak snaží snížit velikost kódu.
Dále chceme zakompilovat demo aplikaci, která bude hodnoty z převodníku číst a vypisovat na terminál. V Application Configuration → Examples tak povolíme ADC Example ( CONFIG_EXAMPLES_ADC), změníme Number of Samples per Group na 1 a povolíme Use software Trigger ( CONFIG_EXAMPLES_ADC_SWTRIG). Důsledky jednotlivých voleb si ukážeme za chvíli při představení kódu aplikace.
Po nahrání nového firmware si v adresáři /dev můžeme všimnout nově zaregistrovaného ovladače adc0. Spustíme demo aplikaci adc, na terminálu bychom měli vidět data, která nám jdou z převodníku.
Sample: 1: channel: 1 value: 541 Sample: 1: channel: 1 value: 621 Sample: 1: channel: 1 value: 636 Sample: 1: channel: 1 value: 588 Sample: 1: channel: 1 value: 577
Náš ADC převodník převádí data z pinu PC0 (kvůli mapování v BSP, na které se za chvíli podíváme), což je na NUCLEO-L476RG desce analogový vstup A5. Můžeme zkusit propojit drátkem A5 a GND, měli bychom vidět hodnoty 0.
Sample: 1: channel: 1 value: 0 Sample: 1: channel: 1 value: 0 Sample: 1: channel: 1 value: 0 Sample: 1: channel: 1 value: 0
Pokud propojíme pin A5 a 3.3 V, uvidíme naopak hodnoty až 4095 (převodník má rozlišení 12 bitů, jde tedy o maximální hodnotu, kterou může vracet).
1: channel: 1 value: 4091 Sample: 1: channel: 1 value: 4090 Sample: 1: channel: 1 value: 4090 Sample: 1: channel: 1 value: 4091
Pojďme se podívat, jak je aplikace vlastně napsaná. Zdrojový kód najdeme v souboru examples/adc/adc_main.c. Kromě volání funkce open na ovladač /dev/adc0 zde máme volání ioctl spouštějící převod analogových dat na digitální a následně jejich čtení přes funkci read.
struct adc_msg_s sample[CONFIG_EXAMPLES_ADC_GROUPSIZE];
#ifdef CONFIG_EXAMPLES_ADC_SWTRIG
/* Issue the software trigger to start ADC conversion */
ret = ioctl(fd, ANIOC_TRIGGER, 0);
if (ret < 0)
{
int errcode = errno;
printf("adc_main: ANIOC_TRIGGER ioctl failed: %d\n", errcode);
}
#endif
/* Read up to CONFIG_EXAMPLES_ADC_GROUPSIZE samples */
readsize = CONFIG_EXAMPLES_ADC_GROUPSIZE * sizeof(struct adc_msg_s);
nbytes = read(fd, sample, readsize);
Všimněme si, že ioctl se volá pouze v případě povoleného CONFIG_EXAMPLES_ADC_SWTRIG (což jsme udělali). Další možností by bylo nespouštět konverzi softwarově, ale použít například výstup z timer periferie či PWM signál. Aplikace by pak volala pouze read a o spouštění konverze by se starala sama periferie. Pro tyto způsoby zatím v NuttXu neexistuje standardní cesta, nýbrž jde o implementaci závislou na mikrokontroléru. Často se spouští povolením příslušných voleb v konfiguraci. Ve struktuře adc_msg_s dostaneme číslo kanálu a raw hodnotu z převodníku, kterou aplikace vypíše.
printf("%d: channel: %d value: %" PRId32 "\n",
i + 1, sample[i].am_channel, sample[i].am_data);
Definici této struktury najdeme v souboru include/nuttx/analog/adc.h, API dokumentace je dostupná na stránkách NuttXu.
begin_packed_struct struct adc_msg_s
{
uint8_t am_channel; /* The 8-bit ADC Channel */
int32_t am_data; /* ADC convert result (4 bytes) */
} end_packed_struct;
drivers
Nyní se podíváme, jak je ADC periferie implementována v drivers sekci. Jedná se o vyšší, obecnou, vrstvu ovladače. Zdrojový kód najdeme v souboru drivers/analog/adc.c. Vyšší vrstva se v tomto případě nepoužívá jen pro předávání ioctl volání do vrstvy nižší, jako tomu bylo v minulém článku představeném ovladači LED, ale implementuje i další funkce. Hlavní z nich je ukládání přečtených dat do bufferu (jde o cyklickou frontu), aplikace tedy při volání funkce read nečte data z registrů mikrokontroléru, ale přistupuje pouze k softwarové frontě ve vyšší vrstvě ovladače. To je užitečná vlastnost, pokud nespouštíme konverzi převodníku ručně, ale například pomocí PWM signálu. Naše volání read v aplikaci totiž bude blokující (pokud tak otevřeme registrovaný ovladač) a k navrácení hodnot dojde až po skončení konverze dat a jejich překopírování do softwarové fronty. Z aplikace můžeme také chtít s frontou manipulovat pomocí ioctl volání, ANIOC_RESET_FIFO nám frontu uvolní, ANIOC_SAMPLES_ON_READ řekne, kolik položek ve frontě zbývá. Nic nám také nebrání otevřít ovladač v neblokujícím režimu a čekat na data s použitím funkcí poll nebo epoll.
Poznámka: při práci s ADC periferií je potřeba dát pozor na jednu věc: fronta je jedna pro daný ovladač znakových zařízení (character device), jeho otevření z více aplikací tak povede ke kolizím, kdy si aplikace budou navzájem krást data. Častý model v NuttXu je jedna centrální aplikace, která čte hodnoty z převodníku a distribuuje je do dalších procesů.
Z kódu níže vidíme, že dvě již zmíněné ioctl se řeší přímo ve vyšší vrstvě ovladače a zbylé nedefinované volání se přeposílají do nižší vrstvy. Jde například o nám již známe ANIOC_TRIGGER či jakékoliv další volání. To otevírá vrátka pro volání specifická pro daný mikrokontrolér či převodník, ačkoliv tento přístup není preferován z důvodu snížení kompatibility aplikací mezi různými deskami.
static int adc_ioctl(FAR struct file *filep, int cmd, unsigned long arg)
{
FAR struct inode *inode = filep->f_inode;
FAR struct adc_dev_s *dev = inode->i_private;
int ret;
switch (cmd)
{
case ANIOC_RESET_FIFO:
{
ret = adc_reset_fifo(dev);
}
break;
case ANIOC_SAMPLES_ON_READ:
{
ret = adc_samples_on_read(dev);
}
break;
default:
{
/* Those IOCTLs might be used in arch specific section */
ret = dev->ad_ops->ao_ioctl(dev, cmd, arg);
}
break;
}
return ret;
}
arch
Jako další se podíváme na implementaci nižší vrstvy ovladače. Pro NUCLEO-L476RG desku najdeme zdrojový kód v souboru arch/arm/src/stm32l4/stm32l4_adc.c. Hned na začátku souboru můžeme najdeme definici adc_ops_s, což jsou ukazatele na funkce, které volá vyšší vrstva.
static const struct adc_ops_s g_adcops =
{
.ao_bind = adc_bind,
.ao_reset = adc_reset,
.ao_setup = adc_setup,
.ao_shutdown = adc_shutdown,
.ao_rxint = adc_rxint,
.ao_ioctl = adc_ioctl,
};
Stejně tak má nižší vrstva zaregistrovaná volání do vrstvy vyšší. To zajišťuje funkce adc_bind definovaná ve vyšší vrstvě.
static int adc_bind(struct adc_dev_s *dev,
const struct adc_callback_s *callback)
{
#ifndef CONFIG_STM32L4_ADC_NOIRQ
struct stm32_dev_s *priv = (struct stm32_dev_s *)dev->ad_priv;
DEBUGASSERT(priv != NULL);
priv->cb = callback;
#endif
return OK;
}
Mezi jedno takové volání patří předání přečtených hodnot do softwarové fronty vyšší vrstvy ( au_receive). Příklad takového zpětného volání najdeme v nižší vrstvě ovladače ve funkci adc_interrupt, kde po přijetí přerušení indikujícího konec konverze dojde k přečtení hodnoty a jejímu předání.
/* EOC: End of conversion */
if ((adcisr & ADC_INT_EOC) != 0)
{
/* Read the converted value and clear EOC bit
* (It is cleared by reading the ADC_DR)
*/
value = adc_getreg(priv, STM32L4_ADC_DR_OFFSET);
value &= ADC_DR_MASK;
/* Verify that the upper-half driver has bound its callback functions */
if (priv->cb != NULL)
{
/* Give the ADC data to the ADC driver. The ADC receive() method
* accepts 3 parameters:
*
* 1) The first is the ADC device instance for this ADC block.
* 2) The second is the channel number for the data, and
* 3) The third is the converted data for the channel.
*/
DEBUGASSERT(priv->cb->au_receive != NULL);
priv->cb->au_receive(dev, priv->r_chanlist[priv->current], value);
}
/* Set the channel number of the next channel that will complete
* conversion
*/
priv->current++;
if (priv->current >= priv->rnchannels)
{
/* Restart the conversion sequence from the beginning */
priv->current = 0;
}
}
boards
Poslední na představení nám zbývá neméně důležitá část ovladače v sekci boards. Ta kromě volání inicializačních funkcí nižší a vyšší vrstvy ovladače i rozhoduje o přiřazení pinů ke kanálům převodníku. U většiny mikrokontrolérů totiž umí A/D převodník provádět konverzi z množství analogových vstupů (často třeba i z 12). To je mimochodem i důvod, proč v ADC struktuře čtené v aplikace máme uvedené, k jakému kanálu data patří. Zdrojový kód najdeme v souboru boards/arm/stm32l4/nucleo-l476rg/src/stm32_adc.c.
struct adc_dev_s *adc;
for (i = 0; i < ADC1_NCHANNELS; i++)
{
stm32l4_configgpio(g_adc1_pinlist[i]);
}
adc = stm32l4_adc_initialize(1, g_adc1_chanlist, ADC1_NCHANNELS);
if (adc == NULL)
{
aerr("ERROR: Failed to get ADC interface\n");
return -ENODEV;
}
ret = adc_register("/dev/adc0", adc);
if (ret < 0)
{
aerr("ERROR: adc_register failed: %d\n", ret);
return ret;
}
Jako první se nakonfiguruje GPIO na správnou alternativní funkci, poté se zavolá inicializace nižší vrstvy, která vrátí strukturu ovladače. Struktura obsahuje ukazatele na funkce volané z vyšší vrstvy. Funkce adc_register vyšší inicializuje a zaregistruje ovladač /dev/adc0. Seznam kanálů a k nim přiřazeným pinům je definován o pár řádků výše
static const uint8_t g_adc1_chanlist[ADC1_NCHANNELS] =
{
1
};
static const uint32_t g_adc1_pinlist[ADC1_NCHANNELS] =
{
GPIO_ADC1_IN1
};
Můžeme si také všimnout zvláštních volání aerr k chybovým výpisům. Tato volání můžeme v jádře NuttXu najít často, jde o specifické logovací funkce. Podrobně se na ně podíváme na konci článku. Další zajímavostí je, že v kódu výše volíme cestu, pod kterou chceme mít ovladač zaregistrovaný. Nemusí se tak nutně jmenovat adc0, ale lze použít vlastní pojmenování dle potřeb aplikace. To je bude hodit například u ovladače GPIO, jak si ukážeme později.
Pulzně šířková modulace (PWM)
Jako další si ukážeme fungování periferií ovládajících pulzně šířkovou modulaci (PWM). Díky této periferii můžeme pomocí diskrétní modulace přenášet analogový signál pomocí dvouhodnotového signálu. Mezi častá použití patří například řízení motorů.
Princip rozdělení zdrojových kódů a ovladačů zůstává stejný jako v případě ovladače ADC, na ovladači PWM si tak primárně ukážeme jeho API a možné problémy, na kterých lze v NuttXu při konfigurování ovladačů narazit.
Princip konfigurace zůstává též stejný. Jako první povolíme nižší vrstvu ovladače v menu konfigurace System Type → STM32L4 Peripheral Selection. STM32L4 nemá přímo dedikovanou periferii PWM, ale poskytuje ji jako mód ovladače časovače (timer). Povolíme tedy TIM2 ( CONFIG_STM32L4_TIM2) volbu. Nyní je ještě potřeba tento časovač nastavit na mód pulzně šířkové modulace. V případě STM32L4 bohužel tato volba není přímo v STM32L4 Peripheral Selection, nýbrž potřebujeme jít o úroveň výše, v podmenu Timer Configuration a v něm zaškrtnout TIM2 PWM ( CONFIG_STM32L4_TIM2_PWM volba). Zároveň povolíme hardwarový výstup pomocí volby TIM2 Channel 1 Output ( CONFIG_STM32L4_TIM2_CH1OUT). Mimochodem, volbou TIM2_ADC ( CONFIG_STM32L4_TIM2_ADC) bychom časovač nakonfigurovali do módu, kdy spouští konverzi analogového převodníku.
Dále musíme povolit obecnou vyšší vrstvu ovladače, kterou najdeme v Device Drivers → Timer Driver Support. Zde nás zajímá volba PWM Driver Support ( CONFIG_PWM). Pomocí Number of Output Channels Per Timer ( CONFIG_PWM_NCHANNELS) bychom mohli nakonfigurovat podporu pro více kanálů PWM na jednom ovladači. V našem případě necháme volbu na původní jedničce.
Poslední konfigurační volba bude povolení ukázkové aplikace, již tradičně půjde v menu Application Configuration → Examples a v případě PWM volbu Pulse width modulation (PWM) example ( CONFIG_EXAMPLES_PWM). Náš ovladač je zaregistrován na cestě /dev/pwm1, musíme tedy ještě změnit volbu PWM device path ( CONFIG_EXAMPLES_PWM_DEVPATH). Pomocí dalších voleb lze měnit nastavení aplikace, jde ale pouze o výchozí hodnoty, které půjde měnit z NSH při spuštění pomocí přepínačů aplikace.
Jednotlivé implementace vrstev ovladačů najdeme na v souborech drivers/timers/pwm.c (vyšší vrstva), arch/arm/src/stm32l4/stm32l4_pwm.c (nižší vrstva) a boards/arm/stm32l4/nucleo-l476rg/src/stm32_pwm.c (volání inicializačních funkcí z desky). Právě v poslední zmíněné najdeme kód, který nám ovladač zaregistruje na cestě /dev/pwm1.
#if defined(CONFIG_STM32L4_TIM2_PWM)
pwm = stm32l4_pwminitialize(2);
if (!pwm)
{
aerr("ERROR: Failed to get the STM32L4 PWM lower half\n");
return -ENODEV;
}
/* Register the PWM driver at "/dev/pwm1" */
ret = pwm_register("/dev/pwm1", pwm);
if (ret < 0)
{
aerr("ERROR: pwm_register failed: %d\n", ret);
return ret;
}
#endif
Pokud nyní zkusíme NuttX zkompilovat, měli bychom skončit chybou kvůli nedefinovanému pinu GPIO_TIM2_CH1OUT.
In file included from chip/stm32l4_pwm.c:42:
chip/stm32l4_pwm.h:567:40: error: 'GPIO_TIM2_CH1OUT' undeclared here (not in a function); did you mean 'GPIO_TIM1_CH1OUT'?
567 | # define PWM_TIM2_CH1CFG GPIO_TIM2_CH1OUT
| ^~~~~~~~~~~~~~~~
chip/stm32l4_pwm.c:524:18: note: in expansion of macro 'PWM_TIM2_CH1CFG'
524 | .pincfg = PWM_TIM2_CH1CFG,
| ^~~~~~~~~~~~~~~
make[1]: *** [Makefile:170: stm32l4_pwm.o] Error 1
make[1]: *** Waiting for unfinished jobs....
make: *** [tools/LibTargets.mk:170: arch/arm/src/libarch.a] Error 2
make: *** Waiting for unfinished jobs....
Tohle je bohužel častý problém, na který lze při konfigurování periferií v NuttXu narazit. Spousta mikrokontrolérů totiž umožňuje přiřadit alternativní piny jako vstup nebo výstup dané periferie, ale kód BSP (board support package, neboli kód pro danou desku ve složce boards) tyto piny nedefinuje. Respektive definuje jen pro některé případy. Můžeme se podívat do zdrojového kódu boards/arm/stm32l4/nucleo-l476rg/include/board.h, kde najdeme následující definice.
/* PWM output for full bridge, uses config 1, because port E is N/A on QFP64 * CH1 | 1(A8) 2(E9) * CH2 | 1(A9) 2(E11) * CHN1 | 1(A7) 2(B13) 3(E8) * CHN2 | 1(B0) 2(B14) 3(E10) */ #define GPIO_TIM1_CH1OUT (GPIO_TIM1_CH1OUT_1|GPIO_SPEED_50MHz) #define GPIO_TIM1_CH1NOUT (GPIO_TIM1_CH1N_1|GPIO_SPEED_50MHz) #define GPIO_TIM1_CH2OUT (GPIO_TIM1_CH2OUT_1|GPIO_SPEED_50MHz) #define GPIO_TIM1_CH2NOUT (GPIO_TIM1_CH2N_1|GPIO_SPEED_50MHz)
Kód námi zvolené desky definuje pouze piny pro časovač 1, nikoliv pro časovač 2, na kterém chceme zkoušet pulzně šířkovou modulaci. Potřebovali bychom tak definovat GPIO_TIM2_CH1OUT. K tomu použijeme definice alternativních pinů, které najdeme v arch/arm/src/stm32l4/hardware/stm32l4x6xx_pinmap.h. Umístění hlavičkového souboru je napříč zdrojovým kódem NuttXu konzistentní – mikrokontrolér má vždy adresář hardware, která obsahuje mapování registrů a pinů pro danou variantu čipu. Použitelné piny pro naši periferii jsou následující.
#define GPIO_TIM2_CH1OUT_1 (GPIO_ALT|GPIO_AF1 |GPIO_SPEED_50MHz|GPIO_PUSHPULL|GPIO_PORTA|GPIO_PIN0) #define GPIO_TIM2_CH1OUT_2 (GPIO_ALT|GPIO_AF1 |GPIO_SPEED_50MHz|GPIO_PUSHPULL|GPIO_PORTA|GPIO_PIN5) #define GPIO_TIM2_CH1OUT_3 (GPIO_ALT|GPIO_AF1 |GPIO_SPEED_50MHz|GPIO_PUSHPULL|GPIO_PORTA|GPIO_PIN15)
Zvolíme GPIO_TIM2_CH1OUT_1 na portu PA0 ( A0 na desce). Jednoduše upravíme soubor boards/arm/stm32l4/nucleo-l476rg/include/board.h.
diff --git a/boards/arm/stm32l4/nucleo-l476rg/include/board.h b/boards/arm/stm32l4/nucleo-l476rg/include/board.h index e2c97ddfa0..4b79d2995e 100644 --- a/boards/arm/stm32l4/nucleo-l476rg/include/board.h +++ b/boards/arm/stm32l4/nucleo-l476rg/include/board.h @@ -248,6 +248,7 @@ #define GPIO_TIM1_CH1NOUT GPIO_TIM1_CH1N_1 #define GPIO_TIM1_CH2OUT GPIO_TIM1_CH2OUT_1 #define GPIO_TIM1_CH2NOUT GPIO_TIM1_CH2N_1 +#define GPIO_TIM2_CH1OUT GPIO_TIM2_CH1OUT_1 #define GPIO_LPTIM1_CH1OUT GPIO_LPTIM1_OUT_1 #define GPIO_LPTIM2_CH1OUT GPIO_LPTIM2_OUT_2
Nyní by již kompilace měla projít bez chyb. Aplikaci v příkazové řádce NuttXu spustíme pomocí příkazu pwm, na 5 sekund by se měl zapnout PWM výstup se střídou 50 % a poté vypnout (parametry pochází z výchozích hodnot konfigurace aplikace). Fungování si můžeme jednoduše ověřit pomocí již dříve zkompilovaného příkladu pro vyzkoušení ovladače ADC. Propojíme piny A0 (výstup PWM) a A5 (vstup ADC) a spustíme aplikaci PWM s přepínačem pwm -t 20 &, čímž prodloužíme dobu generování výstupu na 20 sekund. Pomocí znaku & říkáme NuttXu, že má aplikaci spustit na pozadí, nebude nám tak po dobu svého běhu blokovat příkazovou řádku. Díky tomu můžeme hned spustit aplikaci adc, která by měla střídavě vypisovat hodnoty 4095 (PWM výstup aktivní) a 0 (výstup neaktivní).
nsh> pwm -t 20 & nsh> adc
Zdrojový kód aplikace pro řízení pulzně šířkové modulace najdeme v souboru examples/pwm/pwm_main.c. Již tradičně v něm najdeme volání funkce open.
/* Open the PWM device for reading */
fd = open(g_pwmstate.devpath, O_RDONLY);
if (fd < 0)
{
printf("pwm_main: open %s failed: %d\n", g_pwmstate.devpath, errno);
goto errout;
}
Ovládání PWM v NuttXu probíhá pouze pomocí ioctl volání. Konkrétně jde o PWMIOC_SETCHARACTERISTICS, PWMIOC_START a PWMIOC_STOP (celé API lze najít v hlavičkovém souboru include/nuttx/timers/pwm.h, nebo ve webové dokumentaci). Jako první je potřeba nastavit charakteristiky modulace, což se provádí přes strukturu pwm_info_s předanou ve volání PWMIOC_SETCHARACTERISTICS.
struct pwm_chan_s
{
ub16_t duty;
#ifdef CONFIG_PWM_OVERWRITE
bool ch_outp_ovrwr;
bool ch_outp_ovrwr_val;
#endif
#ifdef CONFIG_PWM_DEADTIME
ub16_t dead_time_a;
ub16_t dead_time_b;
#endif
uint8_t cpol;
uint8_t dcpol;
int8_t channel;
};
struct pwm_info_s
{
uint32_t frequency;
struct pwm_chan_s channels[CONFIG_PWM_NCHANNELS];
FAR void *arg;
};
Pro každý kanál můžeme nastavit polaritu modulace, střídu či dead time (dřívější vypnutí a pozdější zapnutí signálu, používané například při řízení H můstků). Frekvenci jde v NuttXu momentálně konfigurovat pouze na úrovni celé periferie, nelze mít různé hodnoty pro různé kanály, přestože to některé mikrokontroléry umožňují. Je třeba také zmínit, že střída se do struktury nevyplňuje v procentech, nýbrž jako raw 16 bitová hodnota. Můžeme se podívat na příklad z aplikace.
memset(&info, 0, sizeof(info));
info.frequency = g_pwmstate.freq;
for (i = 0; i < CONFIG_PWM_NCHANNELS; i++)
{
info.channels[i].channel = g_pwmstate.channels[i];
info.channels[i].duty = g_pwmstate.duties[i] ? \
b16divi(uitoub16(g_pwmstate.duties[i]) - 1, 100) : 0;
}
ret = ioctl(fd, PWMIOC_SETCHARACTERISTICS,
(unsigned long)((uintptr_t)&info));
Po nastavení charakteristik modulace můžeme zapnout výstup pomocí PWMIOC_START.
ret = ioctl(fd, PWMIOC_START, 0);
Analogicky bychom použili PWMIOC_STOP k vypnutí výstupu.
Vstupně výstupní piny (GPIO)
Poslední periferií, na kterou se v dnešním článku podíváme, je ovladač vstupně výstupních pinů, neboli General-Purpose Inputs/Outputs (GPIO). U tohoto ovladače je princip organizace kódu podobný tomu u ovladače LED představeného v minulém článku. Nepoužívá se tedy nižší vrstva v arch, ale její obecná implementace v drivers, která používá zpětná volání do boards.
V konfiguraci pro ovladač GPIO stačí v Device Drivers → IO Expander/GPIO Support povolit volbu GPIO driver ( CONFIG_DEV_GPIO). V Application Configuration → Examples můžeme rovnou povolit GPIO driver example ( CONFIG_EXAMPLES_GPIO), kterým si funkce pinů vyzkoušíme.
NuttX registruje každý pin jako separátní ovladač. Rozhodnutí, které piny se mají zaregistrovat, se provádí v boards sekci, pro naši desku konkrétně v souboru boards/arm/stm32l4/nucleo-l476rg/src/stm32_gpio.c. Funkce stm32l4_gpio_initialize (volaná ze souboru stm32_bringup.c) postupně zaregistruje vstupní a výstupní piny a piny s funkcionalitou přerušení, pokud jsou nakonfigurovány.
int stm32l4_gpio_initialize(void)
{
int pincount = 0;
int i;
#if BOARD_NGPIOIN > 0
for (i = 0; i < BOARD_NGPIOIN; i++)
{
/* Setup and register the GPIO pin */
g_gpin[i].gpio.gp_pintype = GPIO_INPUT_PIN;
g_gpin[i].gpio.gp_ops = &gpin_ops;
g_gpin[i].id = i;
gpio_pin_register(&g_gpin[i].gpio, pincount);
/* Configure the pin that will be used as input */
stm32l4_configgpio(g_gpioinputs[i]);
pincount++;
}
#endif
#if BOARD_NGPIOOUT > 0
for (i = 0; i < BOARD_NGPIOOUT; i++)
{
/* Setup and register the GPIO pin */
g_gpout[i].gpio.gp_pintype = GPIO_OUTPUT_PIN;
g_gpout[i].gpio.gp_ops = &gpout_ops;
g_gpout[i].id = i;
gpio_pin_register(&g_gpout[i].gpio, pincount);
/* Configure the pin that will be used as output */
stm32l4_gpiowrite(g_gpiooutputs[i], 0);
stm32l4_configgpio(g_gpiooutputs[i]);
pincount++;
}
#endif
#if BOARD_NGPIOINT > 0
for (i = 0; i < BOARD_NGPIOINT; i++)
{
/* Setup and register the GPIO pin */
g_gpint[i].stm32gpio.gpio.gp_pintype = GPIO_INTERRUPT_PIN;
g_gpint[i].stm32gpio.gpio.gp_ops = &gpint_ops;
g_gpint[i].stm32gpio.id = i;
gpio_pin_register(&g_gpint[i].stm32gpio.gpio, pincount);
/* Configure the pin that will be used as interrupt input */
stm32l4_configgpio(g_gpiointinputs[i]);
pincount++;
}
#endif
return 0;
}
Registraci do /dev provádí funkce gpio_pin_register. Piny jsou registrovány jako standardní ovladač znakových zařízení, funkce automaticky vytvoří soubor /dev/gpioX, kde X má hodnotu dle proměnné pincount. Budeme tedy mít piny registrované jako /dev/gpio0, /dev/gpio1 a tak dále. To není úplně ideální, pokud bychom například na pinu měli chybový výstup, pak bychom ho mohli chtít pojmenovat třeba /dev/errout. Můžeme ale místo funkce gpio_pin_register použít gpio_pin_register_byname, které lze jako argument dát jméno, pod kterým se má pin registrovat.
Zpětná volání jsou uvedená ve strukturách gpin_ops, gpiout_ops a gpint_ops. Tyto struktury obsahují ukazatele na funkce provádějící čtení či zápis z pinů, například pro výstupní piny budou definice následující.
static const struct gpio_operations_s gpout_ops =
{
.go_read = gpout_read,
.go_write = gpout_write,
.go_attach = NULL,
.go_enable = NULL,
};
static int gpout_read(struct gpio_dev_s *dev, bool *value)
{
struct stm32gpio_dev_s *stm32gpio =
(struct stm32gpio_dev_s *)dev;
DEBUGASSERT(stm32gpio != NULL && value != NULL);
DEBUGASSERT(stm32gpio->id < BOARD_NGPIOOUT);
gpioinfo("Reading...\n");
*value = stm32l4_gpioread(g_gpiooutputs[stm32gpio->id]);
return OK;
}
static int gpout_write(struct gpio_dev_s *dev, bool value)
{
struct stm32gpio_dev_s *stm32gpio =
(struct stm32gpio_dev_s *)dev;
DEBUGASSERT(stm32gpio != NULL);
DEBUGASSERT(stm32gpio->id < BOARD_NGPIOOUT);
gpioinfo("Writing %d\n", (int)value);
stm32l4_gpiowrite(g_gpiooutputs[stm32gpio->id], value);
return OK;
}
Když pomineme kontroly a ladící výstupy, tak implementace obsahuje jen jednoduché volání funkcí stm32l4_gpiowrite a stm32l4_gpioread. Jejich definici bychom našli v arch sekci a jde o implementaci přístupu k hardwaru, tedy čtení či zápis registrů. Volání stejných funkcí jsme už jednou viděli v případě ovladače LED, který ve výsledku jen nastavoval výstupní pin. Volbu pinů k registraci najdeme též v stm32_gpio.c souboru.
#if BOARD_NGPIOIN > 0
/* This array maps the GPIO pins used as INPUT */
static const uint32_t g_gpioinputs[BOARD_NGPIOIN] =
{
GPIO_IN1,
};
static struct stm32gpio_dev_s g_gpin[BOARD_NGPIOIN];
#endif
#if BOARD_NGPIOOUT
/* This array maps the GPIO pins used as OUTPUT */
static const uint32_t g_gpiooutputs[BOARD_NGPIOOUT] =
{
GPIO_OUT1,
};
static struct stm32gpio_dev_s g_gpout[BOARD_NGPIOOUT];
#endif
#if BOARD_NGPIOINT > 0
/* This array maps the GPIO pins used as INTERRUPT INPUTS */
static const uint32_t g_gpiointinputs[BOARD_NGPIOINT] =
{
GPIO_INT1,
};
static struct stm32gpint_dev_s g_gpint[BOARD_NGPIOINT];
#endif
Definici těchto pinů najdeme v souboru boards/arm/stm32l4/nucleo-l476rg/src/nucleo-l476rg.h. Nyní jsme si představili druhý hlavičkový soubor, který v kódu BSP můžeme najít. Rozdíl oproti souboru include/boards.h, kde jsme definoval výstupní pin pro ovladač PWM, je jednoduchý. Hlavičkový soubor nucleo-l476rg.h je dostupný pouze v rámci podsložky src daného BSP, definice z include/boards.h naopak může využívat i arch sekce.
#define GPIO_IN1 (GPIO_INPUT | GPIO_FLOAT | GPIO_PORTB | GPIO_PIN0)
#define GPIO_OUT1 (GPIO_OUTPUT | GPIO_OUTPUT | GPIO_SPEED_50MHz | \
GPIO_OUTPUT_SET | GPIO_PORTB | GPIO_PIN1)
#define GPIO_INT1 (GPIO_INPUT | GPIO_FLOAT | GPIO_PORTB | GPIO_PIN2)
Pro demonstraci na desce NUCLEO-L476RG se nám vyplatí změnit přiřazení pinů. GPIO_IN1N budeme mít na PB4 a GPIO_OUT1 na PB5.
diff --git a/boards/arm/stm32l4/nucleo-l476rg/src/nucleo-l476rg.h b/boards/arm/stm32l4/nucleo-l476rg/src/nucleo-l476rg.h index 3c3a02e763..33d1ec942a 100644 --- a/boards/arm/stm32l4/nucleo-l476rg/src/nucleo-l476rg.h +++ b/boards/arm/stm32l4/nucleo-l476rg/src/nucleo-l476rg.h @@ -250,9 +250,9 @@ * https://www.youtube.com/watch?v=VXsTLzI6idA -- Original video GPIO */ -#define GPIO_IN1 (GPIO_INPUT | GPIO_FLOAT | GPIO_PORTB | GPIO_PIN0) +#define GPIO_IN1 (GPIO_INPUT | GPIO_FLOAT | GPIO_PORTB | GPIO_PIN4) #define GPIO_OUT1 (GPIO_OUTPUT | GPIO_OUTPUT | GPIO_SPEED_50MHz | \ - GPIO_OUTPUT_SET | GPIO_PORTB | GPIO_PIN1) + GPIO_OUTPUT_SET | GPIO_PORTB | GPIO_PIN5) #define GPIO_INT1 (GPIO_INPUT | GPIO_FLOAT | GPIO_PORTB | GPIO_PIN2) #define GPIO_HTS221_INT (GPIO_INPUT | GPIO_FLOAT | GPIO_PORTA | GPIO_PIN10) diff --git a/boards/arm/stm32l4/nucleo-l476rg/src/stm32_gpio.c b/boards/arm/stm32l4/nucleo-l476rg/src/stm32_gpio.c
Nahrajeme do desky nový firmware a propojíme kabelem piny D5 a D4. Přes aplikaci gpio můžeme měnit výstupní pin a pozorovat jeho změnu čtením vstupního pinu.
nsh> gpio /dev/gpio0 Driver: /dev/gpio0 Input pin: Value=1 nsh> gpio -o 0 /dev/gpio1 Driver: /dev/gpio1 Output pin: Value=1 Writing: Value=0 Verify: Value=0 nsh> gpio /dev/gpio0 Driver: /dev/gpio0 Input pin: Value=0 nsh> gpio -o 1 /dev/gpio1 Driver: /dev/gpio1 Output pin: Value=0 Writing: Value=1 Verify: Value=1 nsh> gpio /dev/gpio0 Driver: /dev/gpio0 Input pin: Value=1
Kód aplikace najdeme opět v examples/goio/gpio_main.c. Ovladač pinu otevřeme již tradičně přes open, čtení a zápis pinů probíhá pomocí volání ioctl.
bool invalue; bool outvalue = false; ret = ioctl(fd, GPIOC_WRITE, (unsigned long)outvalue); ret = ioctl(fd, GPIOC_READ, (unsigned long)((uintptr_t)&invalue));
Aplikace kombinující více periferií
Se znalostmi z představených periferií si nyní můžeme zkusit napsat vlastní aplikaci, která bude nastavovat výstupní pin periferie GPIO a pomocí ovladače ADC bude číst jeho výstupní napětí. Aplikaci můžeme buď psát do examples/hello/hello_main.c, nebo v examples můžeme pro aplikaci vytvořit novou složku. Z hello aplikace můžeme nakopírovat Makefile a Kconfig soubory a ty upravit. Dokumentaci pro přídání vlastní aplikace případně najdeme na stránkách NuttXu.
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <nuttx/analog/adc.h>
#include <nuttx/analog/ioctl.h>
#include <nuttx/config.h>
#include <nuttx/ioexpander/gpio.h>
static inline double analog2voltage(double val, double umin, double umax) {
return val * (umax - umin) + umin;
}
int main(int argc, FAR char *argv[]) {
int ret;
int adc_fd = open("/dev/adc0", O_RDONLY);
int gpio_fd = open("/dev/gpio1", O_RDONLY);
while (true) {
char buf[32];
fgets(buf, 32, stdin);
if (*buf == 'q')
break;
int val = strtol(buf, NULL, 10);
if (val != 0 && val != 1)
break;
ioctl(gpio_fd, GPIOC_WRITE, val);
ioctl(adc_fd, ANIOC_RESET_FIFO);
ioctl(adc_fd, ANIOC_TRIGGER, 0);
struct adc_msg_s sample;
read(adc_fd, &sample, sizeof(sample));
printf("Measured %f\n",
analog2voltage((double)sample.am_data / 4095, 0, 3.3));
}
close(gpio_fd);
close(adc_fd);
return 0;
}
Naše aplikace nejprve otevře soubory /dev/adc0 a /dev/gpio1. V nekonečné smyčce pak budeme číst standardní vstup a přes strtol ho převádět na integer o hodnotách 0 nebo 1. Tuto hodnotu pak budeme nastavovat na výstup pinu D5 pomocí ioctl volání GPIOC_WRITE (budeme měnit výstupní hodnotu na pinu mezi 0 V a 3.3 V).
Následně spustíme konverzi analogového převodníku a zavoláme read. Jelikož jsme ovladač otevřeli v blokujícím režimu, tak funkce počká, dokud nejsou v softwarové frontě dostupná převedená data a následně nám je vrátí. Nyní nám zbývá jen vrácenou hodnotu přepočítat na napětí o rozsahu 0 až 3.3 V a vypsat na terminál. Pro měření napětí samozřejmě musíme pin D5 propojit s analogovým vstupem A5. Aplikace by samozřejmě správně měla kontrolovat návratové hodnoty open, ioctl a read volání, pro zjednodušení je ale v tomto příkladu můžeme vynechat. V aplikaci také nesmíme zapomenout na include hlavičkových souborů, abychom měli k dispozici API pro ovládání periferií.
Ladění a odchytávání chyb
Kromě printf a syslog volání má NuttX i vlastní ladící funkce rozdělené na úrovně error, warning a info. Ně ně jsme již narazili v kapitole týkající se ovladače ADC, jde o volání aerr, awarn a ainfo. Tyto funkce jsou konfigurovatelné separátně pro jednotlivé periferie, poskytují tak efektivní způsob, jak si zapnout ladící výstupy jen pro určitou část kódu. Jejich povolení najdeme v Build Setup → Debug Options pod volbou Enable Debug Features ( CONFIG_DEBUG_FEATURES). V této části se také může hodit zapnout Enable Debug Assertions ( CONFIG_DEBUG_ASSERTIONS), které povolí vyhodnocování DEBUGASSERT maker (na ně jsme narazili u ovladače GPIO).
Dále je pro ladění užitečná volba Generate Debug Symbols ( CONFIG_DEBUG_SYMBOLS), díky které můžeme běh NuttXu ladit například za pomoci nástroje GNU Debugger (GDB) či grafického nástroje Data Display Debugger (DDD). Prvně se k desce připojíme pomocí OpenOCD.
openocd -f interface/stlink-v2.cfg -f target/stm32l4x.cfg
Poté můžeme spustit program DDD. Jelikož se jedná o mikrokontrolér založený na architektuře ARM, potřebujeme použít gdb-multiarch debugger.
ddd --debugger gdb-multiarch nuttx
K desce se pak připojíme pomocí příkazu target remote localhost:3333 a můžeme krokovat již pomocí standardních příkazů GDB. Přes příkaz b stm32_bringup můžeme například přidat breakpoint do funkce inicializující periferie desky, následně pomocí monitor reset halt provést reset desky a zastavit vykonávání kódu. Poslání příkazu c nebo dlouze continue pak způsobí pokračování vykonávání kódy až do dosažení breakpoinut, tedy až k funkci stm32_bringup, kde se program zastaví. Příkazy next ( n) a step ( s) pak můžeme vykonávání programu krokovat.
V dalších dílech si ukážeme pokročilejší techniky programování vestavných systémů s NuttXem. Výhledově si představíme základy fungování síťového ovladače včetně vzdáleného připojení ke konzoli přes nástroj telnet, fungování ovladačů sběrnice CAN či práci s LCD displejem.
