\(I^2C\) (vyslovuje se aj-tů-sí nebo aj-skvér-sí) sběrnice je jedním z nejužitečnějších způsobů, jak spojovat středně "chytré" senzory nebo periferie libovolného hardwaru k procesoru. Drobný problém spočívá v tom, že to vypadá jako hardwarová noční můra, nízkoúrovňová interakce a vysokoúrovňové programování současně.
Sběrnice \(I^2C\) je sériová sběrnice, která může být použita ke spojení více zařízení k jednomu řadiči. Je to jednoduchá sběrnice, která používá dva aktivní vodiče: jeden pro data a druhý pro hodiny. Přestože existuje spousta problémů při používání sběrnice protože není dobře standardizovaná a zařízení mohou mezi sebou mít konflikty a dělat věci svým vlastním způsobem, ale je natolik používaná a užitečná, takže ji nemůžeme ignorovat.
Hardwarové základy
Z hardwarového pohledu je \(I^2C\) sběrnice celkem jednoduchá. Má pouze dva signálové vodiče, SDA (data) a SDC (hodiny). Každá z těchto linek je přitažena vhodným rezistorem k napájení na libovolné úrovni, na kterém zařízení pracuje: ať už 3.3V tak 5V. (Je potřeba si vždy přečíst datasheet konkrétného zařízení, protože existují i zařízení, které fungují jenom s napájecím napětím 3.3V a 5V je zničí.) Velikost pull-up rezistorů nejsou nijak kritické, obvyklá hodnota \(4.7k\Omega\) bude vyhovovat.
Pokud nějaké \(I^2C\) zařízení má v sobě zabudované pull-up rezistory, mohou být externí rezistory vypuštěny. Problém může vzniknout, pokud máme na sběrnici více zařízení se zabudovanými pull-up rezistory. V tomto případě musíme nechat zapojené pull-up rezistory jenom na jednom zařízení.
\(I^2C\) sběrnice je sběrnice s otevřeným kolektorem. To znamená, že signál je aktivě stahován na zem, pokud je tranzistor sepnutý. Naopak pokud je tranzistory vypnutý, sběrnice zůstává na vysoké úrovni pomocí pull-up rezistoru. Výhoda tohoto řešení je ta, že více zařízení mohou stáhnout sběrnici dolů ve stejný čas. To znamená, že sběrnice s otevřeným kolektorem je na nízké úrovni, když jedno nebo více zařízení drží sběrnici na nízké úrovni a je na vysoké úrovni, když žádné zařízení není aktivní.
Linka SCL poskytuje hodinové pulsy, které určují rychlost přenosu dat, jeden datový bit na SDA lince pro každý hodinový puls na SCL. Řídící zařízení vždy určuje rychlost hodin, tedy jak rychle se budou data přenášet. Avšak podřízené zařízení může přidržet hodiny na nízké úrovni a tak zpomalit přenos dat. Obvykle má \(I^2C\) sběrnice jednoho řídícího, což je Pico v našem případě a to řídí hodiny a přihlašuje podřízená zařízení k přenosu dat. Mít více řídících na sběrnici je možné, ale to je "vyšší dívčí" a obvykle to není nezbytné.
Ještě potřebujeme vědět, že celá komunikace obvykle probíhá v osmibitových paketech. Řídící pošle paket, adresový rámec, který obsahuje adresu podřízeného zařízení, se kterým chce komunikovat. Každé zařízení má svoji unikátní adresu, která je obvykle sedmibitová, může být i desetibitová a Pico s tím nemá problém. Dále budeme používat sedmibitovou adresaci, protože je rozšířenější.
Jedním z problémů adresace je to, že výrobci zařízení často používají u zařízení jednu adresu natvrdo nebo jednu množinu volitelných adres, což může některou kombinaci \(I^2C\) více zařízení na jedné sběrnici prostě udělat nemožnou.
Sedmibitová adresa zařízení je v adresovém rámci umístěna v horních sedmi bitech a to může být trochu matoucí, protože adresa o které se píše v datasheetu je 0x40 a adresový rámec poslaný do zařízení má adresu 0x80.
Nejnižší bit v adresovém rámci se používá k označení zápisu do zařízení (hodnota 0) nebo ke čtení ze zařízení (hodnota 1).
Po poslání adresového rámce se posílá nebo přijímá datový rámec do zařízení nebo ze zařízení.
Existuje i speciální signál, který označuje začátek a konec komunikace, tím se zabývají knihovní funkce a my se s tím nemusíme trápit.
To je asi v základu všechno co potřebujete obecně vědět o \(I^2C\), abyste mohli začít, ale někdy bude užitečné ponořit se do detailů, pokud to budete potřebovat. Znalost detailů budete potřebovat zcela jistě, pokud budete vaše programy ladit.
Hodinová linka (SCL) a datová linka (SDA) jsou v klidovém stavu na vysoké úrovni. Řídící (master) zahajuje komunikaci signálem Start tak, že stáhne SDA na nízkou úroveň — písmeno S na obrázku dole. Hodiny jsou poté také staženy řídícím dolů a behem tohoto času může linka SDA změnit stav. Přenášený bit se čte uprostřed následující periody, kdy jsou hodiny nahoře — \(B_1\), \(B_2 \dots B_N\) na obrázku. Toto pokračuje, dokud není přenesen poslední bit. Potom jde SDA na vysokou úrověň při zastavených hodinách na vysoké úrovni, čili je poslán stoP bit — písmeno P na obrázku. Všimněte si, že pokud jsou posílána data, tak se SDA nemění, jsou-li hodiny na vysoké úrovni. Změna SDA v průběhu vysokých hodin značí buď Start nebo stoP bit, t.j. hodiny nahoře a padající SDA je Start bit a hodiny nahoře a stoupající SDA je stoP bit:
Rychlost hodin byla původně stanovena na 100 kHz, to je standardní režim (standard mode) a později byla zvýšena na 400 KHz v rychlém režimu (fast mode). V praxi je v datasheetu zařízení vždy napsáno, jaké kmitočty hodiny zařízení umí zpracovat. U některých zařízení může být kmitočet hodin až 1MHz (ultra fast mode).
\(I^2C\) na Picu
Pico má dva \(I^2C\) řadiče, I2C0 a I2C1, které mohou fungovat jako řídící i podřízené zařízení.
Spojení z obou dvou řadičů může být přesměrováno na různé GPIO piny podle následující tabulky:
| I2C0 | SDA | GP0 | GP4 | GP8 | GP12 | GP16 | GP20 | GP28 | |
|---|---|---|---|---|---|---|---|---|---|
SCL |
GP1 |
GP5 |
GP9 |
GP13 |
GP17 |
GP21 |
| I2C1 | SDA | GP2 | GP6 | GP10 | GP14 | GP18 | GP20 | GP22 | GP26 |
|---|---|---|---|---|---|---|---|---|---|
SCL |
GP3 |
GP7 |
GP11 |
GP15 |
GP19 |
GP21 |
GP27 |
Výběr pinu, který bude fungovat jako I2C se provádí funkcí:
gpio_set_function(gpio, GPIO_FUNC_I2C);
To je potřeba nastavit jak pro pin SDA tak i SCL.
Nelze použít pro jednu \(I^2C\) sběrnici více pinů současně.
Můžete také použít funkci gpio_pull_up(gpio) k nastavení vestavěného pull-up rezistoru, je ale lepší použít externí rezistor, protože vestavěný pull-up rezistor bude přitahovat jenom k 3.3V, což může vadit u zařízení s napájením 5V.
Funkce pro práci s \(I^2C\) sběrnicí
V Pico SDK máme funkce pro inicializaci, konfiguraci a pro zápis a čtení registrů zařízení. Podívejme se na jednotlivé skupiny. Platí, že do našeho zdrojového souboru musíme vložit hlavičkový soubor
#include "hardware/i2c.h"
a do CMakeLists.txt knihovnu hardware_i2c
target_link_libraries(mujprogram pico_stdlib hardware_i2c)
Inicializace
Máme dvě funkce pro inicializaci a vypnutí sběrnice \(I^2C\):
uint i2c_init( i2c_inst_t *i2c, uint baudrate );
void i2c_deinit( i2c_inst_t *i2c );
Funkci i2c_init používáme k nastavení kmitočtu hodin (parametr baudrate) v Hz.
I2C řadič můžeme určit buď hardwarovou adresou anebo pomocí indexu 0 nebo 1.
Makra preprocesoru i2c0 a i2c1 můžeme použít namísto psaní hardwarové adresy, nebo lze ke konverzi použít funkci:
static uint i2c_hw_index(i2c_inst_t *i2c);
Konfigurace
Konfiguraci můžeme udělat několika způsoby.
Funkce i2ci_set_baudrate je nejlepší způsob nastavení rychlosti hodin, protože vrací skutečně nastavenou frekvenci.
uint i2c_set_baudrate( i2c_inst_t *i2c, uint baudrate );
Pico neumí režim ultra rychlé režimy a proto maximální frekvence může být 1MHz.
Funkce i2c_set_slave_mode určuje, zda bude Pico vystupovat jako řídící (master) nebo podřízený (slave) na sběrnici.
Implicitně funguje jako řídící.
void i2c_set_slave_mode( i2c_inst_t *i2c, bool slave, uint8_t addr );
V této funkci také nstavujete i2c adresu, pomocí které bude podřizený (Pico) odpovídat.
Zápis
SDK obsahuje množství velmi podobných funkcí pro zápis, základní je:
int i2c_write_blocking( i2c_inst_t *i2c, uint8_t addr, const uint8_t *src, size_t len, bool nostop );
První parametr určuje který I2C řadič budeme používat a druhý parametr je adresa podřízeného zařízení, do kterého chceme zapisovat.
Další určuje buffer s bajty dat, které pošleme a jejich délku. Návratová hodnota funkce je počet zapsaných bajtů. Pokud se nepodaří zapsat do zařízení, funkce vrací chybu PICO_ERROR_GENERIC, což je indikátor, že se zařízením nejde komunikovat.
Poslední parametr nostop potřebuje malé vysvětlení.
Když zavoláme tuto funkci, ta nejprve pošle rámec s adresou, čili bajt s adresou addr. Tato sedmibitová adresa je posunuta doleva a nejnižsí bit je vynulován, což označuje operaci zápis.
Takže pokud zapisujeme do zařízení s adresou 0x40, na logickém analyzátoru uvidíme 0x80, čili 0x40 << 1.
Po adresovém rámci se posílají datové rámce zadané parametry src a len.
Poslední parametr nostop určuje, jestli se pošle stop bit nebo ne.
Obvyklá transakce zápisu vypadá takto:
START|ADDR|ACK|DATA0|ACK|
DATA1|ACK|
...
DATAn|ACK|STOP
Všimněte si, že podřízený posílá ACK bit, pokud přijal data v pořádku a může poslat NAK, pokud se mu to nepovedlo.
Dále si všimněte, že je zde jeden STOP bit na konci transakce a to nastane, když nastavíte parametr nostop na false.
Jestliže nastavíte parametr nostop na true, tak se konečný STOP bit nepošle a další přenos dat bude pokračovat jako část jedné a téže transakce.
Všimněte si, že vícebajtový přenos (nahoře) vypadá zcela jinak než posílání jednoho bajtu v jedné transakci:
START|ADDR|ACK|DATA0|ACK|STOP START|ADDR|ACK|DATA1|ACK|STOP ... START|ADDR|ACK|DATAn|ACK|STOP
Tady posíláme mnoho adresových rámců spolu se START a STOP bity. V praxi to znamená, že se musíme podívat do datového listu zařízení a zjistit, kolik bajtů je v jedné operaci zařízení schopno přijmout. Nemůžeme spoléhat na to, že budeme schopni poslat najednou to samé množství bajtů rozdělených na části.
Ostatní funkce fungují podobným způsobem s drobnými variacemi. Dvě funkce nám umožnují nastavit čas vypršení (timeout)
int i2c_write_timeout_us( i2c_inst_t *i2c, uint8_t addr, const uint8_t *src, size_t len, bool nostop, uint timeout_us ); (1)
int i2c_write_blocking_until( i2c_inst_t *i2c, uint8_t addr, const uint8_t *src, size_t len, bool nostop, absolute_time_t until ); (2)
| 1 | Tato funkce umožňuje nastavit timeout v mikrosekundách. |
| 2 | Tato funkce umožňuje nastavit timeout dokud není dosaženo určitého absolutního času. |
Hardware má 16 bajtový buffer jak pro příjem, tak i pro poslání dat. Standardní funkce používají tento buffer a proto se vrací okamžitě, pokud není buffer plný. Stav zaplnění bufferu pro zápis můžeme zjistit funkcí:
static size_t i2c_get_write_available( i2c_inst_t *i2c );
do bufferu můžeme poslat data pomocí funkce:
static void i2c_write_raw_blocking( i2c_inst_t *i2c, const uint8_t *src, size_t len );
Zápis do registru
Obvyklý způsob komunikace mezi řídícím a podřízeným spočívá v zápisu dat do registru. To není nic speciálního co se týká \(I^2C\) sběrnice, je to jenom posílání obyčejných dat. Avšak v datasheetech a u programátorů se přemýšlí trochu jinak. Tam se zapisují data do interního úložného prostoru na podřízeném zařízení, t.j. do registru na zařízení. Fakticky mnohá zařízení mají interní úložiště, dokonce některá \(I^2C\) zařízení, například \(I^2C\) EEPROM nejsou nic jiného, než jenom interní úložiště.
V tomto případě obvyklá transakce zápisu do registru je:
-
Pošleme adresový rámec.
-
Pošleme datový rámec s příkazem výběru konkrétního registru.
-
Pošleme datový rámec obsahující bajt nebo dva bajty, které se zapíší do registru.
Takže například můžete napsat:
uint8_t buf[]={ registerAddress, data };
int i2c_write_blocking( i2c, addr, buf, 2, false );
Příkaz k zápisu (registerAddress) závisí na zařízení a musíte se nejdříve podívat do datového listu.
Všimněte si ještě, že transakce bude začínat bitem START s končit bitem STOP.
Na programování \(I^2C\) pamětí EEPROM se můžete podívat sem.
Čtení
Funkce pro čtení jsou podobné funkcím zápisu. Nejdůležitější je:
int i2c_read_blocking( i2c_inst_t *i2c, uint8_t addr, uint8_t *dst, size_t len, bool nostop );
a její parametry znamenají to samé jako u odpovídající funkce pro zápis (samozřejmě komě dst — to je ukazatel úsek paměti v Picu, kam budeme přečtená data ukládat).
Tato funkce pošle nejprve adresový rámes a potom čte z podřízeného zařízení tolik bajtů, kolik je napsáno v parametru len.
Podobně jako u zapisovacích funkcí hardwarová adresa addr je posunuta o bit doleva a uvolněný nejspodnější bit je nastaven na 1, což znamená operaci čtení.
Takže pokud vaše podřízené zařízení má adresu 0x40, operace čtení pošle ve skutečnosti 0x81 — to je důležité si pamatovat, pokud budeme analyzovat datový přenos pomocí logického analyzátoru.
Hlavní problém s touto funkcí čtení spočívá v tom, že pokud podřízené zařízení nepošle dostatek dat, tak se funkce zablokuje navždy. Proto máme k dispozici funkce, kde lze nastavit timeout:
static int i2c_read_timeout_us( i2c_inst_t *i2c, uint8_t addr, uint8_t *dst, size_t len, bool nostop, uint timeout_us);
int i2c_read_blocking_until( i2c_inst_t *i2c, uint8_t addr, uint8_t *dst, size_t len, bool nostop, absolute_time_t until);
První funkce se vrátí nepojději po uplynutí timeout_us mikrosekund, druhá funkce se vrátí nejpozději po dosažení absolutního času until.
Kanál pro čtení má opět 16 bajtový buffer a můžete zjistit, kolik je v něm volného místa funcí:
static size_t i2c_get_read_available( i2c_inst_t *i2c );
a můžete také poslat data přímo do bufferu pomocí funkce
static void i2c_read_raw_blocking( i2c_inst_t *i2c, uint8_t *dst, size_t len );
Transakce čtení vypadá takto:
START|ADDR|ACK|DATA0|ACK|
|DATA1|ACK|
|DATA2|ACK|
...
|DATAn|NAK|STOP
Nadřízený posílá adresový rámec a podřízený odpovídá ACK po přijetí adresy a tím potvrzuje, že adresu dostal a je připraven odesílat data. Potom podřízený posílá data po jednom bajtu a nadřízený je potvrzje pomocí ACK jako odpověď na každý bajt. Nakonec, když už nadřízený přečetl všechno, co potřeboval, tak potvrdí pomocí NAK a pošle STOP bit. Tímto způsobem nadřízený řídí, kolik bajtů dat má být přeneseno.
U blokového přenosu n bajtů dat je to trochu jinak, nemusí se posílak finální STOP bit, to se nastavuje parametrem nostop na hodnotu true.
Čtení registru
Podobně jako zápis do registru je čtení z registru velice běžná operace, jenom trochu komplikovanější, protože nejdříve potřebujem zapsat a potom číst. To znamená, že nejdříve musíme zapsat adresu registru, který chceme číst do zařízení a potom můžeme číst data, které zařízení pošle jako obsah registru. Takže například můžeme použít toto:
char buf[]={registerAddress};
i2c_write_blocking( i2c0, 0x40, buf, 1, false );
i2c_read_blocking( i2c0, 0x40, buf, 1, false );
Jestliže registr posílá více bajtů najednou, potomje obvykle čteme jeden za druhým v blokovém přenosu bez toho, že bychom posílali pokaždé adresu registru. Všimněte si dále, že nepotlačujeme stop bit mezi čtením a zápisem abychom provedli jednu transakci.
V teorii a v praxi převážně, čtení registru pracuje se stop-start sekvencí, která odděluje volání funcí zápis a čtení bez potlačení stop bitu. Takže přenosová sekvence vypadá takto:
START|ADDR|ACK|REGADDR|ACK|STOP|
START|ADDR|ACK|DATA1|ACK|
|DATA2|ACK|
...
|DATAn|NAK|STOP
Protokol pro pomalé čtení
Hodiny na \(I^2C\) sběrnici jsou řízeny masterem a to vyvolává otázku, jak si poradíme s rychlostí, kterou je nebo není schopen zvládnout podřízený (slave).
Jsou dva přístupy, jak počkat na data na \(I^2C\) sběrnici.
První je jednoduše poslat žádost o data a potom provádět čtení ve smyčce. Pokud není podřízené zařízení připravené poslat data, pošle v datovém rámci nastavený bit NAK.
V tomto případě funkce čtení z SDK vrátí záporné číslo, tedy chybu, místo počtu přečtených bajtů. Takže nám stačí sledovat zápornou návratovou hodnotu funkce i2c_read_blocking().
Ovšem čtecí smyčka nesmí být příliš "těsná".
Čas na odpověď zařízení je často dostatečně dlouhý na to, abychom mohli v programu dělat něco jiného a \(I^2C\) sběrnice umožňuje pracovat s jiným podřízeným,
zatímco aktivované zařízení se snaží doručit požadovaná data. Jenom nesmíme zapomenout tato data později přečíst.
Druhý způsob spočívá v tom, že dovolíme podřízenému zařízení pozdržet hodiny na nízké úrovni po tom, co je nadřízený uvolní — tomu se říká "protahování hodin".
Reálné zařízení
Při používání jakéhokoliv \(I^2C\) zařízení máme dva problémy, které je potřeba vyřešit.
-
Musíme správně zapojit vodiče mezi Picem (\(I^2C\) master) a zařízením (\(I^2C\) slave)
-
a zjistit jak napsat program, který bude dělat to, co potřebujeme.
Čidlo pro měření teploty a tlaku vzduchu Bosch Sensortec BMP280

Problém č.1 u tohoto zařízení je trochu složitější, protože čidlo může pracovat jako \(I^2C\) zařízení anebo může používat SPI sběrnici. Záleží na zapojení samotného obvodu BMP280. Proto je potřeba si velmi pozorně přečíst datový list, kde je podrobně popsáno, jak má zapojení vypadat. Datový list je napsán velmi podrobně i co se týká programování a je dobré si ho přečíst celý. V datovém listě je napsáno, že napájecí napětí (Sensor supply voltage \(V_{DD}\) a Interface supply voltage \(V_{DDIO}\)) by mělo být typicky 1.8V maximálně 3.6V. Dvě napájecí napětí jsou: \(V_{DD}\) pro vlastní měřící sensor a \(V_{DDIO}\) pro rozhraní. V našem případě budou spojená v jedno. Pokud napájecí napětí překročí 4.25V, dojde ke zničení obvodu. Z toho nám plyne, že nemůžeme obvod BMP280 napájet z 5V, ale budeme ho napájet z 3.3V (pin 36).

Existuje ještě čidlo BME280 od stejné firmy, které umí navíc měřit relativní vlhkost vzduchu, ale je podstatně dražší.
Problém č.2 se nejprve řeší tak, že nejprve prohledáme Internet a zkusíme zjistit, zda to už někdo nenaprogramoval a můžeme jeho kód použít. V případě čidla BMP280 existuje přímo příklad v Pico SDK, ale my se pokusíme postupovat tak, jako bychom nic nenašli, protože tak pochopíme mnohem více.
Při programování \(I^2C\) je důležité nejprve zjistit hardwarovou adresu zařízení, obvykle to bývá napsáno v datovém listu. Pokud hardwarovou adresu nemůžeme najít, tak lze použít užitečnou pomůcku, která nám pomůže.
Pomůcka pro zjištení \(I^2C\) adresy
V Pico SDK je velmi užitečný program pro zjišťování \(I^2C\) adresy zařízení. Používám ho nejenom pro zjištění neznámého zařízení, ale i jako testovací pomůcku správného zapojení všeho.
/**
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
// Proskenujeme všechny 7-bit I2C adresy, abychom zjistili, zda se někde nenachází
// nějaké podřízené zařízení. Tabulka bude vypadat takto:
//
// I2C Bus Scan
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
// 00 . . . . . . . . . . . . . . . .
// 10 . . @ . . . . . . . . . . . . .
// 20 . . . . . . . . . . . . . . . .
// 30 . . . . @ . . . . . . . . . . .
// 40 . . . . . . . . . . . . . . . .
// 50 . . . . . . . . . . . . . . . .
// 60 . . . . . . . . . . . . . . . .
// 70 . . . . . . . . . . . . . . . .
// T.j. na adresách 0x12 a 0x34 něco je.
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/binary_info.h"
#include "hardware/i2c.h"
#define I2C i2c1 (1)
#define PIN_SDA 14 (2)
#define PIN_SCL 15 (3)
// I2C rezervuje některé adresy pro speciální účely. Vynecháme je ze skenování.
// Rezervované adresy jsou ve formě 000 0xxx nebo 111 1xxx
bool reserved_addr(uint8_t addr) {
return (addr & 0x78) == 0 || (addr & 0x78) == 0x78;
}
int main() {
// Zapneme UART, abychom mohli tisknout na standardní výstup
stdio_init_all();
getchar(); (4)
i2c_init(I2C, 100 * 1000);
gpio_set_function(PIN_SDA, GPIO_FUNC_I2C);
gpio_set_function(PIN_SCL, GPIO_FUNC_I2C);
gpio_pull_up(PIN_SDA); (5)
gpio_pull_up(PIN_SCL); (6)
printf("\nI2C Bus Scan\n");
printf(" 0 1 2 3 4 5 6 7 8 9 A B C D E F\n");
for (int addr = 0; addr < (1 << 7); ++addr) {
if (addr % 16 == 0) {
printf("%02x ", addr);
}
// Uděláme 1 bajtové čtení z testované adresy. Pokud podřízené zařízení
// pošle potvrzení z této adresy, funkce i2c_read_blocking vrátí počet
// přečtených bajtů. Jestliže se na adrese nic nenachází funkce vrátí -1.
int ret;
uint8_t rxdata;
if (reserved_addr(addr)) // Přeskočíme rezervované adresy.
ret = PICO_ERROR_GENERIC;
else
ret = i2c_read_blocking(I2C, addr, &rxdata, 1, false);
printf(ret < 0 ? "." : "@");
printf(addr % 16 == 15 ? "\n" : " ");
}
printf("Hotovo.\n");
return 0;
}
| 1 | Tady si musím nastavit, zda budu používat i2c0 nebo i2c1 |
| 2 | Nastavení SDA pinu. |
| 3 | Nastavení SCL pinu. |
| 4 | Funkce getchar() slouží k tomu, aby mi výpis "neujel", pokud se mi nepodaří rychle spustit minicom. getchar() čeká na nějaký znak na vstupu konzole, prostě stisknu Enter a program se rozjede. |
| 5 | Pokud máme externí pull-up rezistor nebo je pull-up rezistor na zařízení, je potřeba řádek zakomentovat. |
| 6 | Pokud máme externí pull-up rezistor nebo je pull-up rezistor na zařízení, je potřeba řádek zakomentovat. |
Vítejte v programu minicom 2.9
VOLBY: I18n
Port /dev/ttyACM0, 14:14:42
Stiskněte CTRL-A Z pro nápovědu o klávesách se zvláštním významem
I2C Bus Scan
0 1 2 3 4 5 6 7 8 9 A B C D E F
00 . . . . . . . . . . . . . . . .
10 . . . . . . . . . . . . . . . .
20 . . . . . . . . . . . . . . . .
30 . . . . . . . . . . . . . . . .
40 . . . . . . . . . . . . . . . .
50 . . . . . . . . . . . . . . . .
60 . . . . . . . . . . . . . . . .
70 . . . . . . @ . . . . . . . . .
Done.
První program
Po zapojení a odzkoušení libovolného \(I^2C\) zařízení, první otázka ke zodpovězení je, jak to funguje? Naneštěstí pro složitější zařízení je to proces s více kroky. První program se pokusí přečíst nějaká data z BMP280.
V datasheetu se píše, že \(I^2C\) adresa zařízení je 0x76, což nám ukazal i náš program.
Je zde i pěkná tabulka s popisem jednotlivých registrů, kterou si musíme prostudovat, abychom věděli, jak nastavit režim měření a jak číst data.

Na první pohled se může tabulka zdát nepřehlednou, ale je to poměrně jednoduché.
Registry obarvené žlutou barvou obsahují naměřené hodnoty teploty (temp_xlsb, temp_lsb a temp_msb) a tlaku (press_xlsb, press_lsb a press_msb).
Zkratky znamenají toto:
-
msb(most significant byte) — bajt horní -
lsb(last significant byte) — bajt dolní -
xlbs( extended last significant byte) — bajt hodně dole (něco jako desetiny)
Registry obarvené azurovou barvou jsou tzv kalibrační registry a obsahují kalibrační data pro konkrétní čip dodaná od výrobce, který provedl vlastní kalibraci.
Tyto údaje budou pro nás velmi důležité a nebude možné bez nich číst nějaké smyslupné hodnoty.
Registry config a ctrl_meas (bílá barva) slouží k nastavení jednak způsobu měření (počet měření za měřící cyklus) a také jak se zařízení bude nebo nebude uspávat a dále k nastavení filtrů.
BMP280 provádí během měřícího cyklu několik měření a filtry slouží k tomu, aby byly z měření vyloučeny chybné hodnoty.
Dále bude uveden stručný výtah z datasheetu, týkající se registrů podrobněji.
| Registr | Popis |
|---|---|
0xD0 "id" |
obsahuje identifikaci čipu, hodnota je |
0xE0 "reset" |
Zápisem hodnoty |
0xF3 "status" |
obsahuje 2 bity, které označují stav zařízení. Pokud je bit 3 1, zařízení měří a pokud je 0, znamená to, že měření je ukončeno a můžeme odečítat hodnoty. |
0xF4 "ctrl_meas" |
Nastavuje získávání dat ze měřícího zařízení a nastavení režimu odběru (normal, sleep a forced). |
0xF5 "config" |
Nastavuje četnost měření, filtry a volby pro zařízení. Zápis do registru v normálním režimu může být ingorován, ve sleep režimu ignorován není. |
0xF7…0xF9 "press" |
Registr obsahuje nezpracovaná data měření tlaku. |
0xFA…0xFC "temp" |
Registr obsahuje nezpracovaná data měření teploty. |
0xA1…0x88 calib25…calib00 |
Kalibrační data nastavená výrobcem u každého čipu pro přesné měření. |
| bity | popis |
|---|---|
7,6,5 |
převzorkování dat teploty: 000 — neměří se; 001 — 1x; 010 — 2x; 011 — 4x; 100 — 8x; 101,110,111 — 16x |
4,3,2 |
převzorkování data tlaku: |
1,0 |
režim odběru: 00 — sleep mode; 01 nebo 10 — forced mode; 11 — normal_mode |
Režimy odběru BMP280
BMP280 může pracovat ve třech režimech odběru proudu, který se nastavuje v registru mode[1:0]
| hodmota mode[1:0] | režim |
|---|---|
00 |
sleep mode |
01 nebo 10 |
forced mode |
11 |
normal mode |
-
sleep mode Tento režim je nastaven po resetu. V tomto režimu není prováděno žádné měření a odběr proudu (\(I_{DDSM}\)) je minimální. Všechny registry jsou přístupné, registr
ida kompenzační koeficienty je možné přečíst. -
forced mode V tomto režimu je provedeno jedno měření podle nastavených voleb měření a filtrů. Když je měření hodnot dokončeno, senzor se vrací do spánku (sleep) a výsledky měření jsou přístupné v datových registrech. Aby mohlo být provedeno další měření veličin, force mode musí být znovu nastaven zápisem do registru.
-
normal mode V normálním režimu měření se kontinuálně přepíná mezi aktivním měřením a neaktivním spánkem, čas spánku je definován v \(t_{standby}\) podle následující tabulky. Odběr proudu ve spánku (\(I_{DDSB}\)) je o něco věčí než ve sleep modu. Jakmile je nastaven tento režim a volby způsobu měření a filtrů, můžeme získávat data měření z registrů kontinuálně, bez dalšího zápisu do registru
mode.
| hodnota t_sb[2:0] | \(t_{standby}\) [ms] |
|---|---|
000 |
0.5 |
001 |
62.5 |
010 |
125 |
011 |
250 |
100 |
500 |
101 |
1000 |
110 |
2000 |
111 |
4000 |
Zapnutí nebo reset zařízení
Podle popisu v datasheetu zařízení po resetu nebo po zapnutí nezačne samo měřit, protože je v režimu spánku (sleep mode). Musíme tedy nastavit režim měření, t.j nastavit co a jak rychle chceme měřit. To znamená s jakou periodou a s jakou přesností chceme měřit tlak, teplotu a nebo oboje najednou. Zařízení je na velmi slušné úrovni (není to žádna hračka, je to profesionální čidlo určené do mobilů, meteostanic, výtahů atd.) a umí filtrovat nejrůžnější šumy a poruchy měření. My si nejprve zkusíme zařízení vyresetovat a potom udělat jedno měření v režimu force mode. Tento režim se používá v meteorologických stanicích napálených z baterie, kde se požaduje co nějnižší odběr a měří se třeba jednou za 15 minut.
Reset zařízení by měl jít udělat tak, že do registru 0xE0 neboli reset pošleme hodnotu 0xB6.
Potom bychom z registru 0xD0 neboli id měli přečíst hodnotu 0x58. Bude-li to fungovat, víme že nám BM280 funguje dobře.
#include <stdio.h>
#include "hardware/i2c.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"
// kterou i2c budeme používat
#define I2C i2c0
// piny
#define PIN_SDA 14 (2)
#define PIN_SCL 15 (3)
// BMP280 má I2C adresu 0x76
#define ADDR _u(0x76)
// hardwarové registry
#define REG_CONFIG _u(0xF5)
#define REG_CTRL_MEAS _u(0xF4)
#define REG_RESET _u(0xE0)
#define REG_CHIP_ID _u(0xD0)
// teplota
#define REG_TEMP_XLSB _u(0xFC)
#define REG_TEMP_LSB _u(0xFB)
#define REG_TEMP_MSB _u(0xFA)
// tlak
#define REG_PRESSURE_XLSB _u(0xF9)
#define REG_PRESSURE_LSB _u(0xF8)
#define REG_PRESSURE_MSB _u(0xF7)
void bmp280_reset()
{
// reset zařízení
uint8_t buf[2] = { REG_RESET, 0xB6 };
i2c_write_blocking(I2C, ADDR, buf, 2, false);
}
uint8_t bmp280_read_chip_id()
{
uint8_t buf[1] = { 0x0 };
uint8_t reg = REG_CHIP_ID;
i2c_write_blocking(I2C, ADDR, ®, 1, true); // true - přidržíme sběrnici, chceme hned číst
i2c_read_blocking(I2C, ADDR, buf, 1, false); // false - uvolním sběrnici
return buf[0];
}
int main( void )
{
i2c_init(I2C, 100 * 1000);
gpio_set_function(PIN_SDA, GPIO_FUNC_I2C);
gpio_set_function(PIN_SCL, GPIO_FUNC_I2C);
gpio_pull_up(PIN_SDA);
gpio_pull_up(PIN_SCL);
bmp280_reset();
sleep_ms(20);
printf("BMP280 ma identifikator %#u\n", bmp280_read_chip_id() );
}
BM280 ma identifikator chipu: 0x58
Podle výpisu z minicomu nám zařízení funguje, což je dobré.
\(I^2C\) v akci
Pokud máte po ruce logický analyzátor (já jsem si pořídil dva), tak lze pěkně vidět, co se na sběrnici děje.


Čtení teploty a tlaku
Zařízení po resetu nezačne samo měřit, protože je v režimu spánku (sleep mode). Proto musíme po naběhnutí nastavit normální režim měření, čas spánku (\(t_{standby}\)) do registru 0xf5 config a způsob měření.
Použijeme doporučené nastavení z datasheetu (tabulka 15 na straně 19).
Zatím nebudeme využívat kalibraci čidla.
#include <stdio.h>
#include "hardware/i2c.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"
// BMP280 má I2C adresu 0x76
#define ADDR _u(0x76)
// kterou i2c budeme používat
#define I2C i2c0
// piny
#define PIN_SDA 14 (2)
#define PIN_SCL 15 (3)
// hardwarové registry
#define REG_CONFIG _u(0xF5)
#define REG_CTRL_MEAS _u(0xF4)
#define REG_RESET _u(0xE0)
#define REG_TEMP_XLSB _u(0xFC)
#define REG_TEMP_LSB _u(0xFB)
#define REG_TEMP_MSB _u(0xFA)
#define REG_PRESSURE_XLSB _u(0xF9)
#define REG_PRESSURE_LSB _u(0xF8)
#define REG_PRESSURE_MSB _u(0xF7)
void bmp280_init_normal()
{
// použijeme nastavení podle datasheetu "handheld device dynamic"
uint8_t buf[2];
// 500ms čas měření, x16 filtr
const uint8_t reg_config_val = ((0x04 << 5) | (0x05 << 2)) & 0xFC;
// pošleme do registru 0xF5 hodnotu reg_config_val
buf[0] = REG_CONFIG;
buf[1] = reg_config_val;
i2c_write_blocking(i2c_default, ADDR, buf, 2, false);
// převzorkování teploty osrs_t x1, převzorkování tlaku osrs_p x4, režim normal mode
const uint8_t reg_ctrl_meas_val = (0x01 << 5) | (0x03 << 2) | (0x03);
buf[0] = REG_CTRL_MEAS;
buf[1] = reg_ctrl_meas_val;
i2c_write_blocking(i2c_default, ADDR, buf, 2, false);
}
void bmp280_read_raw(int32_t* temp, int32_t* pressure)
{
// BMP280 data registers are auto-incrementing and we have 3 temperature and
// pressure registers each, so we start at 0xF7 and read 6 bytes to 0xFC
// note: normal mode does not require further ctrl_meas and config register writes
uint8_t buf[6];
uint8_t reg = REG_PRESSURE_MSB;
i2c_write_blocking(I2C, ADDR, ®, 1, true); // true to keep master control of bus
i2c_read_blocking(I2C, ADDR, buf, 6, false); // false - finished with bus
// uložíme přečtených 20 bitů do 32 bitového signed integeru pro další konverzi
*pressure = (buf[0] << 12) | (buf[1] << 4) | (buf[2] >> 4);
*temp = (buf[3] << 12) | (buf[4] << 4) | (buf[5] >> 4);
}
int main( void )
{
i2c_init(I2C, 100 * 1000);
gpio_set_function(PIN_SDA, GPIO_FUNC_I2C);
gpio_set_function(PIN_SCL, GPIO_FUNC_I2C);
gpio_pull_up(PIN_SDA);
gpio_pull_up(PIN_SCL);
bmp280_init_normal();
int32_t raw_temperature;
int32_t raw_pressure;
sleep_ms(250); // sleep so that data polling and register update don't collide
while(1) {
bmp280_read_raw(&raw_temperature, &raw_pressure);
printf("Teplota: %d, Tlak: %d", raw_temperature, raw_pressure );
}
}
Teplota: 511456, Tlak: 342288
Teplota: 511456, Tlak: 342288
Teplota: 511440, Tlak: 342276
Teplota: 511424, Tlak: 342280
Na výstupu se nám objeví nějaké hodnoty, které jsou ovšem naprosto nesmyslné. Je to tím, že jsme nepoužili doporučený přepočet a kalibrační data. Musíme to napravit.
Zpracování dat
Čidlo BM280 obsahuje 25 tzv. kalibračních registrů, které obsahují korekce k naměřené hodnotě ADC převodníku. V každém obvodu jsou tyto hodnoty jiné. Na začátku programu si musíme tato data načíst. Uložíme je do struktury:
struct bmp280_calib_param {
// kalibrační parametry pro teplotu
uint16_t dig_t1;
int16_t dig_t2;
int16_t dig_t3;
// kalibrační parametry pro tlak
uint16_t dig_p1;
int16_t dig_p2;
int16_t dig_p3;
int16_t dig_p4;
int16_t dig_p5;
int16_t dig_p6;
int16_t dig_p7;
int16_t dig_p8;
int16_t dig_p9;
};
Tato data slouží i k přepočtu naměřených hodnot na smysluplné hodnoty teploty a tlaku.
Ke čtení dat použijeme funkci bmp280_get_calib_params(), která přečte všechna kalibrační data najednou.
// kalibrační registry
#define REG_DIG_T1_LSB _u(0x88)
#define REG_DIG_T1_MSB _u(0x89)
#define REG_DIG_T2_LSB _u(0x8A)
#define REG_DIG_T2_MSB _u(0x8B)
#define REG_DIG_T3_LSB _u(0x8C)
#define REG_DIG_T3_MSB _u(0x8D)
#define REG_DIG_P1_LSB _u(0x8E)
#define REG_DIG_P1_MSB _u(0x8F)
#define REG_DIG_P2_LSB _u(0x90)
#define REG_DIG_P2_MSB _u(0x91)
#define REG_DIG_P3_LSB _u(0x92)
#define REG_DIG_P3_MSB _u(0x93)
#define REG_DIG_P4_LSB _u(0x94)
#define REG_DIG_P4_MSB _u(0x95)
#define REG_DIG_P5_LSB _u(0x96)
#define REG_DIG_P5_MSB _u(0x97)
#define REG_DIG_P6_LSB _u(0x98)
#define REG_DIG_P6_MSB _u(0x99)
#define REG_DIG_P7_LSB _u(0x9A)
#define REG_DIG_P7_MSB _u(0x9B)
#define REG_DIG_P8_LSB _u(0x9C)
#define REG_DIG_P8_MSB _u(0x9D)
#define REG_DIG_P9_LSB _u(0x9E)
#define REG_DIG_P9_MSB _u(0x9F)
// počet kalibračních registrů
#define NUM_CALIB_PARAMS 24
void bmp280_get_calib_params(struct bmp280_calib_param* params) {
// nezpracovaná data teploty a tlaku musí být kalibrována podle
// parametrů vytvořených výrobcem sensoru
// 3 parametry pro teplotu a 9 parametru pro tlak, pro každý parametr
// registr MSB a LSB, což je celkem 24 registrů
uint8_t buf[NUM_CALIB_PARAMS] = { 0 };
uint8_t reg = REG_DIG_T1_LSB;
i2c_write_blocking(I2C, ADDR, ®, 1, true); // true to keep master control of bus
// read in one go as register addresses auto-increment
i2c_read_blocking(I2C, ADDR, buf, NUM_CALIB_PARAMS, false); // false, we're done reading
// store these in a struct for later use
params->dig_t1 = (uint16_t)(buf[1] << 8) | buf[0];
params->dig_t2 = (int16_t)(buf[3] << 8) | buf[2];
params->dig_t3 = (int16_t)(buf[5] << 8) | buf[4];
params->dig_p1 = (uint16_t)(buf[7] << 8) | buf[6];
params->dig_p2 = (int16_t)(buf[9] << 8) | buf[8];
params->dig_p3 = (int16_t)(buf[11] << 8) | buf[10];
params->dig_p4 = (int16_t)(buf[13] << 8) | buf[12];
params->dig_p5 = (int16_t)(buf[15] << 8) | buf[14];
params->dig_p6 = (int16_t)(buf[17] << 8) | buf[16];
params->dig_p7 = (int16_t)(buf[19] << 8) | buf[18];
params->dig_p8 = (int16_t)(buf[21] << 8) | buf[20];
params->dig_p9 = (int16_t)(buf[23] << 8) | buf[22];
}
Funkci použijeme a vypíšeme si kalibrační hodnoty
int main( void )
{
stdio_init_all();
getchar();
i2c_init(I2C, 100 * 1000);
gpio_set_function(PIN_SDA, GPIO_FUNC_I2C);
gpio_set_function(PIN_SCL, GPIO_FUNC_I2C);
gpio_pull_up(PIN_SDA);
gpio_pull_up(PIN_SCL);
bmp280_init_normal();
int32_t raw_temperature;
int32_t raw_pressure;
// kompenzační parametry z čidla
struct bmp280_calib_param params;
bmp280_get_calib_params(¶ms);
sleep_ms(250); // sleep so that data polling and register update don't collide
// tisk kalibračních parametrů
printf("\ndig_t1: %u\n", params.dig_t1);
printf("dig_t2: %u\n", params.dig_t2);
printf("dig_t3: %u\n", params.dig_t3);
printf("dig_p1: %u\n", params.dig_p1);
printf("dig_p2: %u\n", params.dig_p2);
printf("dig_p3: %u\n", params.dig_p3);
printf("dig_p4: %u\n", params.dig_p4);
printf("dig_p5: %u\n", params.dig_p5);
printf("dig_p6: %u\n", params.dig_p6);
printf("dig_p7: %u\n", params.dig_p7);
printf("dig_p8: %u\n", params.dig_p8);
printf("dig_p9: %u\n", params.dig_p9);
while(1) {
bmp280_read_raw(&raw_temperature, &raw_pressure);
printf("RAW: Teplota: %d, Tlak: %d\n", raw_temperature, raw_pressure );
sleep_ms(500);
}
}
Výstup programu
dig_t1: 27468
dig_t2: 25818
dig_t3: 50
dig_p1: 38004
dig_p2: 4294956474
dig_p3: 3024
dig_p4: 7403
dig_p5: 4294967171
dig_p6: 4294967289
dig_p7: 15500
dig_p8: 4294952696
dig_p9: 6000
RAW: Teplota: 509248, Tlak: 341396
RAW: Teplota: 509248, Tlak: 341396
RAW: Teplota: 509264, Tlak: 341388
RAW: Teplota: 509280, Tlak: 341388
RAW: Teplota: 509296, Tlak: 341388
RAW: Teplota: 509312, Tlak: 341384
RAW: Teplota: 509312, Tlak: 341380
RAW: Teplota: 509344, Tlak: 341380
RAW: Teplota: 509344, Tlak: 341380
Podle datasheetu (kapitola 3.9 a dále od strany 20 ), data je potřeba přepočítat podle doporučeného algoritmu.
// Returns temperature in DegC, resolution is 0.01 DegC. Output value of “5123” equals 51.23
DegC.
// t_fine carries fine temperature as global value
BMP280_S32_t t_fine;
BMP280_S32_t bmp280_compensate_T_int32(BMP280_S32_t adc_T)
{
BMP280_S32_t var1, var2, T;
var1 = ((((adc_T>>3) – ((BMP280_S32_t)dig_T1<<1))) * ((BMP280_S32_t)dig_T2)) >> 11;
var2 = (((((adc_T>>4) – ((BMP280_S32_t)dig_T1)) * ((adc_T>>4) – ((BMP280_S32_t)dig_T1))) >> 12) * ((BMP280_S32_t)dig_T3)) >> 14;
t_fine = var1 + var2;
T = (t_fine * 5 + 128) >> 8;
return T;
}
// Returns pressure in Pa as unsigned 32 bit integer in Q24.8 format (24 integer bits and 8
fractional bits).
// Output value of “24674867” represents 24674867/256 = 96386.2 Pa = 963.862 hPa
BMP280_U32_t bmp280_compensate_P_int64(BMP280_S32_t adc_P)
{
BMP280_S64_t var1, var2, p;
var1 = ((BMP280_S64_t)t_fine) – 128000;
var2 = var1 * var1 * (BMP280_S64_t)dig_P6;
var2 = var2 + ((var1*(BMP280_S64_t)dig_P5)<<17);
var2 = var2 + (((BMP280_S64_t)dig_P4)<<35);
var1 = ((var1 * var1 * (BMP280_S64_t)dig_P3)>>8) + ((var1 * (BMP280_S64_t)dig_P2)<<12);
var1 = (((((BMP280_S64_t)1)<<47)+var1))*((BMP280_S64_t)dig_P1)>>33;
if (var1 == 0) {
return 0; // avoid exception caused by division by zero
}
p = 1048576 - adc_P;
p = (((p<<31)-var2)*3125)/var1;
var1 = (((BMP280_S64_t)dig_P9) * (p>>13) * (p>>13)) >> 25;
var2 = (((BMP280_S64_t)dig_P8) * p) >> 19;
p = ((p + var1 + var2) >> 8) + (((BMP280_S64_t)dig_P7)<<4);
return (BMP280_U32_t)p;
}
// Returns temperature in DegC, double precision. Output value of “51.23” equals 51.23 DegC.
// t_fine carries fine temperature as global value
BMP280_S32_t t_fine;
double bmp280_compensate_T_double(BMP280_S32_t adc_T)
{
double var1, var2, T;
var1 = (((double)adc_T)/16384.0 – ((double)dig_T1)/1024.0) * ((double)dig_T2);
var2 = ((((double)adc_T)/131072.0 – ((double)dig_T1)/8192.0) *
(((double)adc_T)/131072.0 – ((double) dig_T1)/8192.0)) * ((double)dig_T3);
t_fine = (BMP280_S32_t)(var1 + var2);
T = (var1 + var2) / 5120.0;
return T;
}
// Returns pressure in Pa as double. Output value of “96386.2” equals 96386.2 Pa = 963.862 hPa
double bmp280_compensate_P_double(BMP280_S32_t adc_P)
{
double var1, var2, p;
var1 = ((double)t_fine/2.0) – 64000.0;
var2 = var1 * var1 * ((double)dig_P6) / 32768.0;
var2 = var2 + var1 * ((double)dig_P5) * 2.0;
var2 = (var2/4.0)+(((double)dig_P4) * 65536.0);
var1 = (((double)dig_P3) * var1 * var1 / 524288.0 + ((double)dig_P2) * var1) / 524288.0;
var1 = (1.0 + var1 / 32768.0)*((double)dig_P1);
if (var1 == 0.0)
{
return 0; // avoid exception caused by division by zero
}
p = 1048576.0 – (double)adc_P;
p = (p – (var2 / 4096.0)) * 6250.0 / var1;
var1 = ((double)dig_P9) * p * p / 2147483648.0;
var2 = p * ((double)dig_P8) / 32768.0;
p = p + (var1 + var2 + ((double)dig_P7)) / 16.0;
return p;
}
Firma Bosch Sensortec dodává k dispozici celé SDK pro různé systémy, my si musíme funkci malinko upravit, protože 64 bitový integer na Picu nemáme a počítání v plovoucí řádové čárce je také problematické. Naše funkce bude tato:
// intermediate function that calculates the fine resolution temperature
// used for both pressure and temperature conversions
int32_t bmp280_convert(int32_t temp, struct bmp280_calib_param* params) {
// use the 32-bit fixed point compensation implementation given in the
// datasheet
int32_t var1, var2;
var1 = ((((temp >> 3) - ((int32_t)params->dig_t1 << 1))) * ((int32_t)params->dig_t2)) >> 11;
var2 = (((((temp >> 4) - ((int32_t)params->dig_t1)) * ((temp >> 4) - ((int32_t)params->dig_t1))) >> 12) * ((int32_t)params->dig_t3)) >> 14;
return var1 + var2;
}
int32_t bmp280_convert_temp(int32_t temp, struct bmp280_calib_param* params) {
// uses the BMP280 calibration parameters to compensate the temperature value read from its registers
int32_t t_fine = bmp280_convert(temp, params);
return (t_fine * 5 + 128) >> 8;
}
int32_t bmp280_convert_pressure(int32_t pressure, int32_t temp, struct bmp280_calib_param* params) {
// uses the BMP280 calibration parameters to compensate the pressure value read from its registers
int32_t t_fine = bmp280_convert(temp, params);
int32_t var1, var2;
uint32_t converted = 0.0;
var1 = (((int32_t)t_fine) >> 1) - (int32_t)64000;
var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * ((int32_t)params->dig_p6);
var2 += ((var1 * ((int32_t)params->dig_p5)) << 1);
var2 = (var2 >> 2) + (((int32_t)params->dig_p4) << 16);
var1 = (((params->dig_p3 * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) + ((((int32_t)params->dig_p2) * var1) >> 1)) >> 18;
var1 = ((((32768 + var1)) * ((int32_t)params->dig_p1)) >> 15);
if (var1 == 0) {
return 0; // nesmíme dělit nulou
}
converted = (((uint32_t)(((int32_t)1048576) - pressure) - (var2 >> 12))) * 3125;
if (converted < 0x80000000) {
converted = (converted << 1) / ((uint32_t)var1);
} else {
converted = (converted / (uint32_t)var1) * 2;
}
var1 = (((int32_t)params->dig_p9) * ((int32_t)(((converted >> 3) * (converted >> 3)) >> 13))) >> 12;
var2 = (((int32_t)(converted >> 2)) * ((int32_t)params->dig_p8)) >> 13;
converted = (uint32_t)((int32_t)converted + ((var1 + var2 + params->dig_p7) >> 4));
return converted;
}
Doplnění o displej
Naše zapojení doplníme o OLED displej, abychom mohli měření používat bez počítače. Displej musíme dát na druhou \(I^2C\) sběrnici, protože společně obě zařízení nechtějí na jedné sběrnici fungovat. Displej chce funguovat na 400 kHz a čidlo BMP280 chce fungovat na 1 MHz hodinového kmitočtu. Kód pro zobrazování na displeji SSD1306 už máme hotový.

Celý výpis programu
/**
* Měření teploty a tlaku
*
* SPDX-License-Identifier: BSD-3-Clause
**/
#include <stdio.h>
#include "hardware/i2c.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"
#include "ssd1306.h"
#include "font_spleen_16x8a.h"
// display
#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 32
#define OLED_ADDR 0x3C
#define I2C_FREQ 400000
#define SLEEPTIME 55
#define DISPL_PIN_SDA 18 // piny pro dispej
#define DISPL_PIN_SCL 19
// BMP280 má I2C adresu 0x76
#define ADDR _u(0x76)
// kterou i2c budeme používat
#define I2C i2c0
// piny
#define PIN_SDA 4 // piny pro čidlo BM280
#define PIN_SCL 5 //
// hardware registry
#define REG_CONFIG _u(0xF5)
#define REG_CTRL_MEAS _u(0xF4)
#define REG_RESET _u(0xE0)
#define REG_TEMP_XLSB _u(0xFC)
#define REG_TEMP_LSB _u(0xFB)
#define REG_TEMP_MSB _u(0xFA)
#define REG_PRESSURE_XLSB _u(0xF9)
#define REG_PRESSURE_LSB _u(0xF8)
#define REG_PRESSURE_MSB _u(0xF7)
// kalibrační registry
#define REG_DIG_T1_LSB _u(0x88)
#define REG_DIG_T1_MSB _u(0x89)
#define REG_DIG_T2_LSB _u(0x8A)
#define REG_DIG_T2_MSB _u(0x8B)
#define REG_DIG_T3_LSB _u(0x8C)
#define REG_DIG_T3_MSB _u(0x8D)
#define REG_DIG_P1_LSB _u(0x8E)
#define REG_DIG_P1_MSB _u(0x8F)
#define REG_DIG_P2_LSB _u(0x90)
#define REG_DIG_P2_MSB _u(0x91)
#define REG_DIG_P3_LSB _u(0x92)
#define REG_DIG_P3_MSB _u(0x93)
#define REG_DIG_P4_LSB _u(0x94)
#define REG_DIG_P4_MSB _u(0x95)
#define REG_DIG_P5_LSB _u(0x96)
#define REG_DIG_P5_MSB _u(0x97)
#define REG_DIG_P6_LSB _u(0x98)
#define REG_DIG_P6_MSB _u(0x99)
#define REG_DIG_P7_LSB _u(0x9A)
#define REG_DIG_P7_MSB _u(0x9B)
#define REG_DIG_P8_LSB _u(0x9C)
#define REG_DIG_P8_MSB _u(0x9D)
#define REG_DIG_P9_LSB _u(0x9E)
#define REG_DIG_P9_MSB _u(0x9F)
// počet kalibračních registrů
#define NUM_CALIB_PARAMS 24
struct bmp280_calib_param {
// temperature params
uint16_t dig_t1;
int16_t dig_t2;
int16_t dig_t3;
// pressure params
uint16_t dig_p1;
int16_t dig_p2;
int16_t dig_p3;
int16_t dig_p4;
int16_t dig_p5;
int16_t dig_p6;
int16_t dig_p7;
int16_t dig_p8;
int16_t dig_p9;
};
void bmp280_init() {
// use the "handheld device dynamic" optimal setting (see datasheet)
uint8_t buf[2];
// 500ms sampling time, x16 filter
const uint8_t reg_config_val = ((0x04 << 5) | (0x05 << 2)) & 0xFC;
// send register number followed by its corresponding value
buf[0] = REG_CONFIG;
buf[1] = reg_config_val;
i2c_write_blocking(I2C, ADDR, buf, 2, false);
// osrs_t x1, osrs_p x4, normal mode operation
const uint8_t reg_ctrl_meas_val = (0x01 << 5) | (0x03 << 2) | (0x03);
buf[0] = REG_CTRL_MEAS;
buf[1] = reg_ctrl_meas_val;
i2c_write_blocking(I2C, ADDR, buf, 2, false);
}
void bmp280_read_raw(int32_t* temp, int32_t* pressure) {
// BMP280 data registers are auto-incrementing and we have 3 temperature and
// pressure registers each, so we start at 0xF7 and read 6 bytes to 0xFC
// note: normal mode does not require further ctrl_meas and config register writes
uint8_t buf[6];
uint8_t reg = REG_PRESSURE_MSB;
i2c_write_blocking(I2C, ADDR, ®, 1, true); // true to keep master control of bus
i2c_read_blocking(I2C, ADDR, buf, 6, false); // false - finished with bus
// store the 20 bit read in a 32 bit signed integer for conversion
*pressure = (buf[0] << 12) | (buf[1] << 4) | (buf[2] >> 4);
*temp = (buf[3] << 12) | (buf[4] << 4) | (buf[5] >> 4);
}
void bmp280_reset() {
// reset the device with the power-on-reset procedure
uint8_t buf[2] = { REG_RESET, 0xB6 };
i2c_write_blocking(I2C, ADDR, buf, 2, false);
}
// intermediate function that calculates the fine resolution temperature
// used for both pressure and temperature conversions
int32_t bmp280_convert(int32_t temp, struct bmp280_calib_param* params) {
// use the 32-bit fixed point compensation implementation given in the
// datasheet
int32_t var1, var2;
var1 = ((((temp >> 3) - ((int32_t)params->dig_t1 << 1))) * ((int32_t)params->dig_t2)) >> 11;
var2 = (((((temp >> 4) - ((int32_t)params->dig_t1)) * ((temp >> 4) - ((int32_t)params->dig_t1))) >> 12) * ((int32_t)params->dig_t3)) >> 14;
return var1 + var2;
}
int32_t bmp280_convert_temp(int32_t temp, struct bmp280_calib_param* params) {
// uses the BMP280 calibration parameters to compensate the temperature value read from its registers
int32_t t_fine = bmp280_convert(temp, params);
return (t_fine * 5 + 128) >> 8;
}
int32_t bmp280_convert_pressure(int32_t pressure, int32_t temp, struct bmp280_calib_param* params) {
// uses the BMP280 calibration parameters to compensate the pressure value read from its registers
int32_t t_fine = bmp280_convert(temp, params);
int32_t var1, var2;
uint32_t converted = 0.0;
var1 = (((int32_t)t_fine) >> 1) - (int32_t)64000;
var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * ((int32_t)params->dig_p6);
var2 += ((var1 * ((int32_t)params->dig_p5)) << 1);
var2 = (var2 >> 2) + (((int32_t)params->dig_p4) << 16);
var1 = (((params->dig_p3 * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) + ((((int32_t)params->dig_p2) * var1) >> 1)) >> 18;
var1 = ((((32768 + var1)) * ((int32_t)params->dig_p1)) >> 15);
if (var1 == 0) {
return 0; // avoid exception caused by division by zero
}
converted = (((uint32_t)(((int32_t)1048576) - pressure) - (var2 >> 12))) * 3125;
if (converted < 0x80000000) {
converted = (converted << 1) / ((uint32_t)var1);
} else {
converted = (converted / (uint32_t)var1) * 2;
}
var1 = (((int32_t)params->dig_p9) * ((int32_t)(((converted >> 3) * (converted >> 3)) >> 13))) >> 12;
var2 = (((int32_t)(converted >> 2)) * ((int32_t)params->dig_p8)) >> 13;
converted = (uint32_t)((int32_t)converted + ((var1 + var2 + params->dig_p7) >> 4));
return converted;
}
void bmp280_get_calib_params(struct bmp280_calib_param* params) {
// raw temp and pressure values need to be calibrated according to
// parameters generated during the manufacturing of the sensor
// there are 3 temperature params, and 9 pressure params, each with a LSB
// and MSB register, so we read from 24 registers
uint8_t buf[NUM_CALIB_PARAMS] = { 0 };
uint8_t reg = REG_DIG_T1_LSB;
i2c_write_blocking(I2C, ADDR, ®, 1, true); // true to keep master control of bus
// read in one go as register addresses auto-increment
i2c_read_blocking(I2C, ADDR, buf, NUM_CALIB_PARAMS, false); // false, we're done reading
// store these in a struct for later use
params->dig_t1 = (uint16_t)(buf[1] << 8) | buf[0];
params->dig_t2 = (int16_t)(buf[3] << 8) | buf[2];
params->dig_t3 = (int16_t)(buf[5] << 8) | buf[4];
params->dig_p1 = (uint16_t)(buf[7] << 8) | buf[6];
params->dig_p2 = (int16_t)(buf[9] << 8) | buf[8];
params->dig_p3 = (int16_t)(buf[11] << 8) | buf[10];
params->dig_p4 = (int16_t)(buf[13] << 8) | buf[12];
params->dig_p5 = (int16_t)(buf[15] << 8) | buf[14];
params->dig_p6 = (int16_t)(buf[17] << 8) | buf[16];
params->dig_p7 = (int16_t)(buf[19] << 8) | buf[18];
params->dig_p8 = (int16_t)(buf[21] << 8) | buf[20];
params->dig_p9 = (int16_t)(buf[23] << 8) | buf[22];
}
int main() {
char displ_str1[64];
char displ_str2[64];
stdio_init_all();
// i2c pro čidlo
i2c_init(I2C, 100 * 1000);
gpio_set_function(PIN_SDA, GPIO_FUNC_I2C);
gpio_set_function(PIN_SCL, GPIO_FUNC_I2C);
gpio_pull_up(PIN_SDA);
gpio_pull_up(PIN_SCL);
// i2c pro displej
i2c_init(i2c1, 100 * 400);
gpio_set_function(DISPL_PIN_SDA, GPIO_FUNC_I2C);
gpio_set_function(DISPL_PIN_SCL, GPIO_FUNC_I2C);
gpio_pull_up(DISPL_PIN_SDA);
gpio_pull_up(DISPL_PIN_SCL);
// konfigurace BMP280
bmp280_init();
// konfigurace OLED displeje
ssd1306_t disp;
disp.external_vcc = false;
ssd1306_init(&disp, DISPLAY_WIDTH, DISPLAY_HEIGHT, OLED_ADDR, i2c1);
ssd1306_clear(&disp);
// kompenzační parametry z čidla
struct bmp280_calib_param params;
bmp280_get_calib_params(¶ms);
int32_t raw_temperature;
int32_t raw_pressure;
sleep_ms(250); // počkáme, abychom nerozbili měření
while (1) {
bmp280_read_raw(&raw_temperature, &raw_pressure);
int32_t temperature = bmp280_convert_temp(raw_temperature, ¶ms);
int32_t pressure = bmp280_convert_pressure(raw_pressure, raw_temperature, ¶ms);
printf("Atmosférický tlak = %.3f hPa\n", pressure / 100.f);
printf("Teplota = %.2f ˚C\n", temperature / 100.f);
for(int i=0; i<64; i++) {
displ_str1[i]='\0'; displ_str2[i]='\0';
}
sprintf(displ_str1,"At:%.3f hPa", pressure / 100.f);
sprintf(displ_str2,"Tepl:%.2f %cC", temperature / 100.f, 0xb0);
ssd1306_clear(&disp);
ssd1306_draw_string_with_font(&disp, 0, 0, 1, font_spleen_16x8a, displ_str1);
ssd1306_draw_string_with_font(&disp, 0, 16, 1, font_spleen_16x8a, displ_str2);
ssd1306_show(&disp);
// budeme číst data každých 5 sekund
sleep_ms(5000);
}
}