Эта запись в блоге является седьмой из серии постов, в которых я исследую различные периферийные устройства микроконтроллера STM32F401RE, используя встроенный Rust на уровне HAL. Пожалуйста, имейте в виду, что некоторые концепции в новых постах могут зависеть от концепций в предыдущих постах.
Если вам понравился этот материал, пожалуйста, не забудьте подписаться на рассылку новостей здесь, чтобы быть в курсе новых статей блога.
- Введение
- Предварительные знания
- Настройка программного обеспечения
- Установка оборудования
- Материалы
- Подключения
- Дизайн программного обеспечения
- Реализация кода
- Импорт ящиков
- Код конфигурации периферии
- Конфигурация периферии I2C:
- Конфигурация периферийного устройства последовательной связи UART:
- Конфигурация периферийных устройств таймера и задержки:
- Код приложения
- Полный код приложения
- Дальнейшие эксперименты/идеи:
- Заключение
Введение
В этом посте я буду конфигурировать и настраивать периферийное устройство I2C stm32f4xx-hal для сбора данных измерения температуры окружающей среды с цифрового датчика давления BMP180. Обратите внимание, что BMP180 предоставляет данные как о температуре, так и о давлении. Измерение температуры будет непрерывно собираться и отправляться на терминал ПК по UART. Я также буду использовать приложение/код последовательной связи UART из предыдущего сообщения. Кроме того, я не буду использовать никаких прерываний.
🚨 Важное замечание:
BMP180 предоставляет данные с датчиков температуры и давления, которые собираются очень похожим образом. Я решил собирать только данные о температуре, так как уравнения преобразования меньше. Моя цель в этой статье — сосредоточиться на работе с I2C, а не на особенностях самого BMP180. Однако предоставленный код может быть легко расширен для сбора и преобразования данных о давлении.
Предварительные знания
Чтобы понять содержание этой статьи, вам необходимо следующее:
- Базовые знания кодирования на языке Rust.
- Знакомство с базовым шаблоном для создания встроенных приложений в Rust.
- Знакомство с основами коммуникации I2C.
- Знакомство с основами коммуникации UART.
Настройка программного обеспечения
Весь код, представленный в этом посте, а также инструкции по настройке среды и инструментария доступны в git-репо apollolabsdev Nucleo-F401RE. Обратите внимание, что если код в git-репозитории немного отличается, это означает, что он был изменен для улучшения качества кода или с учетом обновлений HAL/Rust.
В дополнение к вышесказанному, вам необходимо установить какой-либо терминал последовательной связи на вашем компьютере. Некоторые рекомендации включают:
Для Windows:
- PuTTy
- Teraterm
Для Mac и Linux:
- minicom
Некоторые инструкции по установке для различных операционных систем можно найти в Discovery Book.
Установка оборудования
Материалы
- Плата Nucleo-F401RE
- Датчик барометрического давления и температуры BMP 180 GY-68
Подключения
- Вывод SCL модуля BMP180 подключен к выводу PB8 платы Nucleo.
- Вывод SDA модуля BMP180 подключен к выводу PB9 платы Nucleo.
- Вывод Vcc модуля BMP180 подключен к выводу 3,3 В платы Nucleo.
- Контакт GND модуля BMP180 подключен к GND платы Nucleo.
- Линия UART Tx, которая подключается к ПК через встроенный USB-мост, проходит через контакт PA2 микроконтроллера. Это жестко подключенный вывод, то есть вы не можете использовать какой-либо другой для этой установки. Если вы не используете другую плату, отличную от Nucleo-F401RE, то для определения номера вывода необходимо обратиться к соответствующей документации (справочному руководству или техническому описанию).
Дизайн программного обеспечения
К счастью, дизайн программного обеспечения для этого приложения можно получить из технического описания BMP180. Если копнуть глубже, то в техническом описании BMP180 содержится полная блок-схема, описывающая алгоритмические шаги. В блок-схеме также приведен список формул преобразования, необходимых для расчета компенсированных температуры и давления. Часть с отображением значения температуры — это, по сути, шаг, на котором я буду отправлять результат по UART.
Из приведенной выше блок-схемы я буду реализовывать только те части, которые связаны со сбором данных о температуре. Этот же код может быть легко расширен и для давления. Нужно будет только реализовать дополнительные уравнения.
Реализация кода
Импорт ящиков
В данной реализации необходимы следующие крейты:
- Крейт
cortex_m_rt
для кода запуска и минимального времени выполнения для микроконтроллеров Cortex-M. - Крейт
core::fmt
позволит нам использовать макросwriteln!
для простой печати. - Крейт
panic_halt
для определения поведения паники, чтобы останавливаться при панике. - Крейт
stm32f4xx_hal
для импорта аппаратных абстракций устройств микроконтроллеров STMicro серии STM32F4 поверх API доступа к периферии.
use core::fmt::Write;
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
i2c::Mode,
pac::{self},
prelude::*,
serial::config::Config,
};
Код конфигурации периферии
Конфигурация периферии I2C:
1️⃣ Получение хэндла для периферийных устройств устройства: Во встроенном Rust, как часть паттерна проектирования singleton, мы сначала должны получить периферийные устройства уровня PAC. Это делается с помощью метода take()
. Здесь я создаю обработчик периферийного устройства с именем dp
следующим образом:
let dp = pac::Peripherals::take().unwrap();
2️⃣ Настройте системные часы: Системные часы должны быть настроены, так как они необходимы для настройки периферийных устройств I2C и UART. Для настройки системных часов нам нужно сначала продвинуть структуру RCC
из PAC и ограничить ее с помощью метода constrain()
(подробнее о методе constrain
здесь), чтобы дать доступ к структуре cfgr
. После этого мы создаем хэндл clocks
, который предоставляет доступ к настроенным (и замороженным) системным часам. Часы настроены на использование частоты HSE 8 МГц путем применения метода use_hse()
к структуре cfgr
. Частота HSE определена в справочном руководстве к плате разработки Nucleo-F401RE. Наконец, метод freeze()
применяется к структуре cfgr
для замораживания конфигурации часов. Обратите внимание, что замораживание часов является защитным механизмом HAL, чтобы избежать изменения конфигурации часов во время выполнения. Из этого следует, что периферийные устройства, которым требуется информация о часах, будут принимать только замороженную конфигурационную структуру Clocks
.
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
2️⃣ Продвигаем GPIO-структуры уровня PAC и получаем хэндлы для пинов SDA и SCL: Здесь мне нужно настроить и получить хэндлы для пинов SDA и SCL, чтобы они могли управляться периферийным устройством I2C. Как было показано ранее, контакты SDA и SCL подключены к PB9 и PB8, соответственно. Поэтому, прежде чем я смогу получить какие-либо ручки, мне нужно раскрутить структуру pac-уровня GPIOB
, чтобы иметь возможность создавать ручки для отдельных выводов. Для этого я использую метод split()
следующим образом:
let gpiob = dp.GPIOB.split();
Далее я получаю хэндлы для sda
и scl
следующим образом:
let scl = gpiob.pb8;
let sda = gpiob.pb9;
3️⃣ Настраиваем периферийный канал I2C: Если посмотреть на распиновку платы Nucleo-F401RE, то выводы PB8 и PB9 линий SDA и SCL подключаются к периферийному устройству I2C1 микроконтроллера. Это означает, что нам необходимо сконфигурировать I2C1 и каким-то образом передать его на ручки выводов, которые мы хотим использовать. Для конфигурирования/включения последовательного периферийного канала у нас есть два варианта, как и в случае с некоторыми другими периферийными устройствами. Первый — использовать хэндл периферийного устройства dp
для прямого доступа к I2C и инстанцировать экземпляр передатчика с помощью метода i2c
из признаков расширения I2C. Второй — использовать метод new
в структуре абстракции I2c
для инстанцирования экземпляра I2C1. Обратите внимание, что оба варианта — это разные способы сделать одно и то же!
Для первого варианта, если мы рассмотрим сигнатуру метода i2c
в признаках расширения I2C, она выглядит следующим образом:
fn i2c<SCL, SDA>(
self,
pins: (SCL, SDA),
mode: impl Into<Mode>,
clocks: &Clocks
) -> I2c<Self, (SCL, SDA)>
Метод принимает три параметра, экземпляр pins в виде кортежа, режим и ссылку на замороженный экземпляр Clocks
. Таким образом, мы можем создать хэндл i2c
для I2C1 следующим образом:
let mut i2c = dp.I2C1.i2c(
(scl, sda),
Mode::Standard {
frequency: 100.kHz(),
},
&clocks,
);
scl
, sda
и clocks
— это дескрипторы, которые мы создали ранее. Mode
— это перечисление, которое содержит информацию о режиме, необходимую для периферийного устройства I2C, и имеет две следующие опции:
pub enum Mode {
Standard {
frequency: Hertz,
},
Fast {
frequency: Hertz,
duty_cycle: DutyCycle,
},
}
В моем случае я выбрал опцию Standard
с частотой 100 кГц для работы. В техническом описании BMP180 указано, что устройство может обрабатывать до 3,4 Мбит/с, поэтому я выбрал произвольное значение ниже заявленного предела. Второй вариант — это опция Fast
, которая позволяет также контролировать рабочий цикл.
Альтернативно, второй вариант с использованием абстракции I2C
выглядит следующим образом:
let mut i2c = I2c::new(
dp.I2C1,
(scl, sda),
Mode::Standard {
frequency: 300.kHz(),
},
&clocks,
);
Видно, что основное различие здесь в том, что new
применяется как метод экземпляра на структуре I2c
. Здесь можно заметить, что метод new
также принимает четвертый параметр, который является экземпляром периферийного устройства I2C I2C1
. Это можно увидеть в сигнатуре метода экземпляра tx
в документации, которая выглядит следующим образом:
pub fn new(
i2c: I2C,
pins: (SCL, SDA),
mode: impl Into<Mode>,
clocks: &Clocks
) -> Self
На этом конфигурация I2C закончена, теперь перейдем к UART.
Конфигурация периферийного устройства последовательной связи UART:
1️⃣ Получите ручку и настройте пин последовательной передачи (Tx): Поскольку кнопкой Tx является PA2
, мне нужно создать хэндл для gpioa
. Однако, поскольку вывод не используется как обычный вход или выход GPIO, это означает, что вывод должен быть подключен к другому периферийному устройству внутри микроконтроллера. Для этого вывод может быть сконфигурирован с помощью метода into_alternate()
следующим образом:
let gpioa = dp.GPIOA.split();
let tx_pin = gpioa.pa2.into_alternate();
2️⃣ Настройте канал последовательной периферии: Если посмотреть на распиновку платы Nucleo-F401RE, то вывод PA2 линии Tx подключается к периферийному устройству USART2 в микроконтроллере. Это означает, что нам необходимо сконфигурировать USART2 и каким-то образом передать его на ручку вывода, который мы хотим использовать. Это делается следующим образом:
let mut tx = dp
.USART2
.tx(
tx_pin,
Config::default()
.baudrate(115200.bps())
.wordlength_8()
.parity_none(),
&clocks,
)
.unwrap();
tx_pin
и clocks
— это хэндлы, которые мы создали ранее. Config
— это тип struct, который содержит конфигурационную информацию, необходимую для настройки периферийного устройства UART. Здесь я создаю экземпляр Config
с признаком default
сначала для настройки параметров по умолчанию. После этого я применяю методы baudrate
, wordlength_8
и parity_none
для настройки периферийного устройства UART на нужные мне параметры. Полный список методов Config
можно найти здесь. Я настроил параметры UART, как показано на рисунке, на скорость 115200 бит/с в бодах с 8 битами данных и без четности, также обычно называемой 8N1. Наконец, поскольку метод tx
возвращает результат, мы должны развернуть его с помощью метода unwrap
.
📝 Примечание:
Более подробно о настройке UART можно прочитать в блоге UART Serial Communication.
Конфигурация периферийных устройств таймера и задержки:
В алгоритме необходимо ввести задержку, чтобы дождаться окончания преобразования АЦП в BMP180. Я буду использовать TIM1
и создам миллисекундную задержку delay
следующим образом:
let mut delay = dp.TIM1.delay_ms(&clocks);
На этом конфигурация закончена! Теперь перейдем к коду приложения.
Код приложения
В описанном программном проекте первый шаг требует, чтобы мы считали кучу калибровочных данных из EEPROM BMP180. Для этого мне нужно создать и инициализировать некую структуру для сохранения калибровочных данных. Я назвал тип struct Coeffs
и определил его следующим образом:
struct Coeffs {
ac5: i16,
ac6: i16,
mc: i16,
md: i16,
}
После этого я создаю calib_coeffs
типа Coeffs
, инициализированный всеми нулями:
let mut calib_coeffs = Coeffs {
ac5: 0,
ac6: 0,
mc: 0,
md: 0,
};
📝 Примечание:
В техническом описании для BMP180 есть 11 различных калибровочных коэффициентов, которые должны быть захвачены. Здесь я фиксирую только те, которые необходимы для расчета температуры.
Далее я определяю кучу констант, которые отражают адреса калибровочных коэффициентов в EEPROM BMP180, I2C адрес самого BMP180 (BMP180_ADDR
) и адрес для получения ID устройства BMP180 (REG_ID_ADDR
).
const BMP180_ADDR: u8 = 0x77;
const REG_ID_ADDR: u8 = 0xD0;
const AC5_MSB_ADDR: u8 = 0xB2;
const AC6_MSB_ADDR: u8 = 0xB4;
const MC_MSB_ADDR: u8 = 0xBC;
const MD_MSB_ADDR: u8 = 0xBE;
const CTRL_MEAS_ADDR: u8 = 0xF4;
const MEAS_OUT_LSB_ADDR: u8 = 0xF7;
const MEAS_OUT_MSB_ADDR: u8 = 0xF6;
Я также определяю две переменные, массив [u8; 2]
, который я назвал rx_buffer
и i16
, названный rx_word
. Я буду использовать rx_buffer
позже для буферизации данных, которые я считываю через I2C с BMP180. rx_word
будет использоваться для реконструирования считанных байтов в 16-битное значение.
let mut rx_buffer: [u8; 2] = [0; 2];
let mut rx_word: i16;
Прежде чем что-то делать, мы должны понять, как BMP180 обменивается данными по I2C. По существу, связь BMP180 работает следующим образом: сначала происходит цикл записи (управления), указывающий, какой внутренний адрес EEPROM BMP180 мы хотим прочитать. Во-вторых, есть цикл чтения, в котором предоставляются данные по адресу, который был запрошен в цикле записи.
Первое, что мне нужно прочитать в соответствии с алгоритмическими шагами — это коэффициенты калибровки. Однако в техническом описании рекомендуется, чтобы перед чтением любых калибровочных коэффициентов из BMP180 был прочитан идентификатор устройства BMP180 в качестве проверки на вменяемость. Устройство должно вернуть значение 0x55
. Для этого, покопавшись в документации по методам I2C, я нашел методы write
и read
со следующими подписями:
pub fn write(&mut self, addr: u8, bytes: &[u8]) -> Result<(), Error>
и
pub fn read(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), Error>
Хотя в документации не было много описания, можно увидеть, что оба они более или менее похожи с небольшим отличием. Обе принимают два аргумента и возвращают Result
. Первый аргумент addr
— это адрес устройства, в нашем случае это будет адрес устройства BMP180 BMP180_ADDR
. Второй аргумент для метода write
— bytes
и представляет собой фрагмент массива, содержащий записываемые байты. Вторым аргументом метода read
является buffer
и представляет собой изменяемый фрагмент массива, содержащий байты, возвращаемые адресуемым устройством.
Мое внимание привлек еще один метод, который показался мне полезным. Это был метод write_read
. И снова, хотя описание было не очень большим, я понял его работу, немного поэкспериментировав. Метод write_read
выполняет цикл записи, за которым сразу же следует цикл чтения. Это будет полезно во многих контекстах и сэкономит несколько строк кода. Однако мы увидим, что мне по-прежнему нужны отдельные методы read
и write
. Метод write_read
имеет следующую сигнатуру:
pub fn write_read(
&mut self,
addr: u8,
bytes: &[u8],
buffer: &mut [u8]
) -> Result<(), Error>
Параметры более или менее те же, что и у предыдущих методов, только объединены в один.
Итак, теперь, когда у меня есть все необходимое, чтобы провести проверку на вменяемость, вот код, который я написал:
i2c.write_read(BMP180_ADDR, &[REG_ID_ADDR], &mut rx_buffer).unwrap();
if rx_buffer[0] == 0x55 {
writeln!(tx, "Device ID is {}r", rx_buffer[0]).unwrap();
} else {
writeln!(tx, "Device ID Cannot be Detected r").unwrap();
}
Анализируя код, первым аргументом является адрес устройства BMP180_ADDR
, вторым аргументом является фрагмент, содержащий адрес получения ID устройства REG_ID_ADDR
, и, наконец, третьим аргументом является буфер приема rx_buffer
. Обратите внимание, что все аргументы, которые я передаю, я создал в более ранней точке приложения. Следующий оператор if
проверяет правильность полученного ID и посылает соответствующее сообщение по UART. Обратите внимание, что, согласно спецификации BMP180, поскольку REG_ID_ADDR возвращает только один байт, мне нужно было проверить только первый индекс в буфере rx_buffer[0]
.
Теперь, когда я убедился, что устройство может быть обнаружено, давайте приступим к первому шагу алгоритма — сбору калибровочных коэффициентов. Вот код для получения калибровочного коэффициента AC5:
i2c.write_read(BMP180_ADDR, &[AC5_MSB_ADDR], &mut rx_buffer)
.unwrap();
rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
writeln!(tx, "AC5 = {} r", rx_word).unwrap();
calib_coeffs.ac5 = rx_word;
Как можно заметить, я использовал метод write_read
точно так же, как и ранее. Однако здесь есть три отличия:
- Помните, я упоминал, что BMP180 предоставляет 16-битное значение для каждого коэффициента калибровки. Поскольку I2C обменивается данными в байтах, каждый коэффициент разбивается на два байта MSB и LSB, каждый из которых имеет свой собственный адрес в EEPROM BMP180. Это означает, что технически мне нужно будет получить каждый адрес отдельно и восстановить 16-битное значение. Однако оказывается, что если послать BMP180 только MSB адрес (
AC5_MSB_ADDR
в данном случае), то устройство отправит обратно MSB и LSB без необходимости отдельно адресовать LSB. MSB будет расположен в первом индексе буфера, а LSB — во втором. Итак, суть в том, что мне нужно было вызвать методread_write
только один раз, используяAC5_MSB_ADDR
для получения какAC5_MSB_ADDR
, так иAC5_LSB_ADDR
. - Поскольку обратно будет предоставлен
i16
, строкаrx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
берет MSB биты изrx_buffer[0]
, приводит их кi16
и сдвигает 1 байт (8 раз) влево, используя< <
, затем OR результат с помощью оператора|
с LSB битами вrx_buffer[1]
, которые также приведены кi16
. Результат сохраняется вrx_word
и затем передается по каналу UART. - Слово
rx_word
хранится в структуреcalib_coeffs
, которую я создал ранее в оператореcalib_coeffs.ac5 = rx_word;
. Это также позволит мне повторно использовать rx_word для следующих операций.
Предыдущий код повторяется точно таким же образом три раза для получения коэффициентов AC6, MC и MD. Очевидно, что единственным отличием будет адрес, который я пошлю BMP180, и имя члена, который я сохраню в структуре calib_coeffs
.
Теперь, когда калибровочные коэффициенты доступны, можно запустить цикл измерения. Первым шагом, как указано в программном проекте, является запуск измерения температуры в BMP180 путем записи 0x2E в регистр с адресом 0xF4 (вводится как CTRL_MEAS_ADDR
). Это делается с помощью двух циклов записи, сначала посылается 0x2E
, а затем CTRL_MEAS_ADDR
. Это может быть сделано и в одной строке. Поскольку метод write
в своей сигнатуре принимает фрагмент в параметре bytes
, все байты, которые необходимо отправить, можно включить в фрагмент следующим образом:
i2c.write(BMP180_ADDR, &[CTRL_MEAS_ADDR, 0x2E]).unwrap();
Этот оператор отправит на BMP180 CTRL_MEAS_ADDR
, за которым последует значение 0x2E
. Для тех, кто знаком с терминами I2C, хотя я не проверял на низком уровне, да и в документации не указано, но здесь должен происходить «повторный старт».
После запуска измерения температуры BMP180, согласно техническому описанию, нам нужно подождать не менее 4,5 мс. Поэтому я жду 5 мс для подстраховки, используя созданный ранее хэндл delay
:
delay.delay_ms(5_u32);
Теперь измерение температуры должно быть готово к сбору данных путем чтения MSB и LSB адресов EEPROM BMP180. Это можно сделать, используя предыдущий подход, когда я посылаю только MSB адрес. Здесь я показываю другой подход, где я считываю MSB и LSB отдельно, просто чтобы доказать, что это работает 😃:
i2c.write(BMP180_ADDR, &[MEAS_OUT_MSB_ADDR]).unwrap();
i2c.read(BMP180_ADDR, &mut rx_buffer).unwrap();
rx_word = (rx_buffer[0] as i16) << 8;
i2c.write(BMP180_ADDR, &[MEAS_OUT_LSB_ADDR]).unwrap();
i2c.read(BMP180_ADDR, &mut rx_buffer).unwrap();
rx_word |= rx_buffer[0] as i16;
Наконец, поскольку полученное значение температуры не скомпенсировано. В техническом описании приведена куча формул для расчета компенсированной температуры на основе калибровочных коэффициентов, которые были собраны ранее. Следующий код реализует формулы из таблицы данных для расчета температуры с последующей отправкой результата по UART:
let x1 = (rx_word as i32 - calib_coeffs.ac6 as i32) * (calib_coeffs.ac5 as i32) >> 15;
let x2 = ((calib_coeffs.mc as i32) << 11) / (x1 + calib_coeffs.md as i32);
let b5 = x1 + x2;
let t = ((b5 + 8) >> 4) / 10;
// Print Temperature Value
writeln!(tx, "Temperature = {:} r", t).unwrap();
Обратите внимание, что значения приводятся к виду i32
, чтобы предотвратить переполнение при операциях умножения.
Вот и все!
Полный код приложения
Здесь представлен полный код реализации, описанной в этом посте. Кроме того, вы можете найти полный проект и другие материалы в git-репо apollolabsdev Nucleo-F401RE.
#![no_std]
#![no_main]
// Imports
use core::fmt::Write;
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
i2c::Mode,
pac::{self},
prelude::*,
serial::config::Config,
};
#[entry]
fn main() -> ! {
// Setup handler for device peripherals
let dp = pac::Peripherals::take().unwrap();
// I2C Config steps:
// 1) Need to configure the system clocks
// - Promote RCC structure to HAL to be able to configure clocks
let rcc = dp.RCC.constrain();
// - Configure system clocks
// 8 MHz must be used for the Nucleo-F401RE board according to manual
let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
// 2) Configure/Define SCL and SDA pins
let gpiob = dp.GPIOB.split();
let scl = gpiob.pb8;
let sda = gpiob.pb9;
// 3) Configure I2C perihperal channel
// We're going to use I2C1 since its pins are the ones connected to the I2C interface we're using
// To configure/instantiate serial peripheral channel we have two options:
// Use the i2c device peripheral handle and instantiate a transmitter instance using extension trait
let mut i2c = dp.I2C1.i2c(
(scl, sda),
Mode::Standard {
frequency: 100.kHz(),
},
&clocks,
);
// Or use the I2C abstraction
// let mut i2c = I2c::new(
// dp.I2C1,
// (scl, sda),
// Mode::Standard {
// frequency: 300.kHz(),
// },
// &clocks,
// );
// Serial config steps:
// 1) Need to configure the system clocks
// Already done earlier for I2C module
// 2) Configure/Define TX pin
// Use PA2 as it is connected to the host serial interface
let gpioa = dp.GPIOA.split();
let tx_pin = gpioa.pa2.into_alternate();
// 3) Configure Serial perihperal channel
// We're going to use USART2 since its pins are the ones connected to the USART host interface
// To configure/instantiate serial peripheral channel we have two options:
// Use the device peripheral handle to directly access USART2 and instantiate a transmitter instance
let mut tx = dp
.USART2
.tx(
tx_pin,
Config::default()
.baudrate(9600.bps())
.wordlength_8()
.parity_none(),
&clocks,
)
.unwrap();
let mut delay = dp.TIM1.delay_ms(&clocks);
struct Coeffs {
ac5: i16,
ac6: i16,
mc: i16,
md: i16,
}
let mut calib_coeffs = Coeffs {
ac5: 0,
ac6: 0,
mc: 0,
md: 0,
};
const BMP180_ADDR: u8 = 0x77;
const REG_ID_ADDR: u8 = 0xD0;
const AC5_MSB_ADDR: u8 = 0xB2;
const AC6_MSB_ADDR: u8 = 0xB4;
const MC_MSB_ADDR: u8 = 0xBC;
const MD_MSB_ADDR: u8 = 0xBE;
const CTRL_MEAS_ADDR: u8 = 0xF4;
const MEAS_OUT_LSB_ADDR: u8 = 0xF7;
const MEAS_OUT_MSB_ADDR: u8 = 0xF6;
let mut rx_buffer: [u8; 2] = [0; 2];
let mut rx_word: i16;
// Read Device ID as Sanity Check
i2c.write(BMP180_ADDR, &[REG_ID_ADDR]).unwrap();
i2c.read(BMP180_ADDR, &mut rx_buffer).unwrap();
// OR
// i2c.write_read(BMP180_ADDR, &[REG_ID_ADDR], &mut rx_buffer)
// .unwrap();
if rx_buffer[0] == 0x55 {
writeln!(tx, "Device ID is {}r", rx_buffer[0]).unwrap();
} else {
writeln!(tx, "Device ID Cannot be Detected r").unwrap();
}
// Read Calibration Coefficients
// Read AC5
i2c.write_read(BMP180_ADDR, &[AC5_MSB_ADDR], &mut rx_buffer)
.unwrap();
rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
writeln!(tx, "AC5 = {} r", rx_word).unwrap();
calib_coeffs.ac5 = rx_word;
// Read AC6
i2c.write_read(BMP180_ADDR, &[AC6_MSB_ADDR], &mut rx_buffer)
.unwrap();
rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
writeln!(tx, "AC6 = {} r", rx_word).unwrap();
calib_coeffs.ac6 = rx_word;
// Read MC
i2c.write_read(BMP180_ADDR, &[MC_MSB_ADDR], &mut rx_buffer)
.unwrap();
rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
writeln!(tx, "MC = {} r", rx_word).unwrap();
calib_coeffs.mc = rx_word;
// Read MD
i2c.write_read(BMP180_ADDR, &[MD_MSB_ADDR], &mut rx_buffer)
.unwrap();
rx_word = ((rx_buffer[0] as i16) << 8) | rx_buffer[1] as i16;
writeln!(tx, "MD = {} r", rx_word).unwrap();
calib_coeffs.md = rx_word;
// Application Loop
loop {
// Kick off Temperature Measurement by writing 0x2E in register 0xF4
i2c.write(BMP180_ADDR, &[CTRL_MEAS_ADDR, 0x2E]).unwrap();
// Wait 4.5 ms for measurment to complete as specified by the datasheet
delay.delay_ms(5_u32);
// Collect Temperature Measurment
// Read Measurement MSB
// Achieving same as above using an alternate method syntax here to do a write followed by read
i2c.write(BMP180_ADDR, &[MEAS_OUT_MSB_ADDR]).unwrap();
i2c.read(BMP180_ADDR, &mut rx_buffer).unwrap();
rx_word = (rx_buffer[0] as i16) << 8;
// Read Measurement LSB
i2c.write(BMP180_ADDR, &[MEAS_OUT_LSB_ADDR]).unwrap();
i2c.read(BMP180_ADDR, &mut rx_buffer).unwrap();
rx_word |= rx_buffer[0] as i16;
// Uncomment following line to print raw uncompenstated temperature value
//writeln!(tx, "UT = {} r", rx_word).unwrap();
// Calculate Temperature According to Datasheet Formulas
let x1 = (rx_word as i32 - calib_coeffs.ac6 as i32) * (calib_coeffs.ac5 as i32) >> 15;
let x2 = ((calib_coeffs.mc as i32) << 11) / (x1 + calib_coeffs.md as i32);
let b5 = x1 + x2;
let t = ((b5 + 8) >> 4) / 10;
// Print Temperature Value
writeln!(tx, "Temperature = {:} r", t).unwrap();
}
}
Дальнейшие эксперименты/идеи:
Некоторые идеи для экспериментов включают:
- Расширить код для сбора данных измерения давления, включая данные калибровки, и вычисления барометрического давления.
- BMP180 имеет различные режимы точности, в которых могут быть получены более точные измерения, но также и разное время ожидания. Вы можете рефакторить код или даже создать функции, которые могут выбирать один из нужных режимов.
Заключение
В этом посте было создано приложение для измерения температуры и давления с помощью I2C, управляющее и собирающее данные с внешнего датчика BMP180. Для этого использовалась периферия I2C для микроконтроллера STM32F401RE на плате разработки Nucleo-F401RE. Полученные результаты измерений также передаются на главный ПК через UART-соединение. Весь код был основан на опросе (без прерываний). Кроме того, весь код был создан на уровне HAL с использованием stm32f4xx Rust HAL. Есть вопросы? Поделитесь своими мыслями в комментариях ниже 👇. Если вы нашли это полезным, не забудьте подписаться на рассылку новостей здесь, чтобы быть в курсе новых статей блога.