目录
1、nRF52xx TWI介绍
TWI总线基础介绍
nRF52系列处理器的I2C总线 叫做 TWI (I2C compatible two-wire interface),I2C兼容双线接口。I2C通讯为半双工,协议的基础知识我们这里就不过多的介绍,主要介绍一下nRF52系列处理器上的I2C资源。
在nRF52832处理器中,有2条 TWI 总线,TWI0 和 TWM1。
如果TWI 作为主机使用 带有 EasyDMA 则称为 TWIM。
如果TWI 作为从机使用 带有 EasyDMA 则称为 TWIS。
- I2C兼容
- 速率可达 100kbps,250kbps 或者 400kbps
- 支持时钟延长
- 带 EasyDmA
一次只能分配一个外设来驱动特定的GPIO引脚,同时为了确保在系统处于关闭模式时 TWI 主机所使用的引脚上正确的电平以及当TWI主机被禁用时,这些引脚必须按照下图所示进行配置:
TWI总线寄存器
TWI 库函数介绍
在这一篇记录中,直接开一个小章节先熟悉一下需要用到的库函数
TWI初始化函数
nrf_drv_twi_init
有4个参数:
第一个参数选择使用哪个 TWI 模块(0或者1) 如果使用 EasyDMA 就是TWIM
第二个参数是 TWI 的配置,使用哪个 IO 口、频率、中断优先级等
第三个参数是 自定义的事件处理函数,如果设置为NULL, 则使能 TWI 的阻塞模式。
第四个参数 写 NULL暂时,不太清楚,也是处理函数?第三个写NULL,所以这个也设置为NULL
/*
第一个参数就是定义使用哪一个 TWI 或者是 TWIM(如果使用EasyDMA的话):
typedef struct
{
uint8_t inst_idx;
union
{
#ifdef TWIM_PRESENT
nrfx_twim_t twim;
#endif
#ifdef TWI_PRESENT
nrfx_twi_t twi;
#endif
} u;
bool use_easy_dma;
} nrf_drv_twi_t;
nrf_drv_twi_t 结构体中的 nrfx_twi_t 结构体
typedef struct
{
NRF_TWI_Type * p_twi; ///< Pointer to a structure with TWI registers.
uint8_t drv_inst_idx; ///< Driver instance index.
} nrfx_twi_t;
nrfx_twi_t 结构体中的 NRF_TWI_Type 就是对应的 TWI 的每个寄存器
第二个参数定义 TWI 的配置:
typedef struct
{
uint32_t scl; ///< SCL pin number.
uint32_t sda; ///< SDA pin number.
nrf_drv_twi_frequency_t frequency; ///< TWI frequency.
uint8_t interrupt_priority; ///< Interrupt priority.
bool clear_bus_init; ///< Clear bus during init.
bool hold_bus_uninit; ///< Hold pull up state on gpio pins after uninit.
} nrf_drv_twi_config_t;
第三个参数是 用户提供的事件处理程序:
第四个参数是 传递给事件处理的上下文?
*/
ret_code_t nrf_drv_twi_init(nrf_drv_twi_t const * p_instance,
nrf_drv_twi_config_t const * p_config,
nrf_drv_twi_evt_handler_t event_handler,
void * p_context)
TWI使能函数
void nrf_drv_twi_enable(nrf_drv_twi_t const * p_instance)
就一个参数,和初始化函数的第一个参数一致,使用哪个 TWI 模块
TWI 主机发送数据给从机 函数
nrf_drv_twi_tx
有5个参数:
第一个参数,和上面介绍的一样,哪个 TWI 模块;
第二个参数 ,从机的地址;
注意从机地址 address,函数接收的是 7位地址,函数内部会自己加上读写位。
第三个参数,指向发送缓冲区的指针,发送的数据(一个字节);
第四个参数,发送数据的长度(发送几个一个字节的数据);
第五个参数, 如果设置了 stop 条件,在传输成功完成以后,总线上不会生成 stop 信号(允许在下一次传输中重复启动)。
/*
nrfx_err_t nrfx_twi_tx(nrfx_twi_t const * p_instance,
uint8_t address,
uint8_t const * p_data,
size_t length,
bool no_stop)
{
nrfx_twi_xfer_desc_t xfer = NRFX_TWI_XFER_DESC_TX(address, (uint8_t*)p_data, length);
return nrfx_twi_xfer(p_instance, &xfer, no_stop ? NRFX_TWI_FLAG_TX_NO_STOP : 0);
}
*/
ret_code_t nrf_drv_twi_tx(nrf_drv_twi_t const * p_instance,
uint8_t address,
uint8_t const * p_data,
uint8_t length,
bool no_stop)
{
ret_code_t result = 0;
if (NRF_DRV_TWI_USE_TWIM)
{
result = nrfx_twim_tx(&p_instance->u.twim,
address, p_data, length, no_stop);
}
else if (NRF_DRV_TWI_USE_TWI)
{
result = nrfx_twi_tx(&p_instance->u.twi,
address, p_data, length, no_stop);
}
return result;
}
TWI 主机从从机读取 函数
nrf_drv_twi_rx
有4个参数:
第一个参数,哪个 TWI 设备;
第二个参数,从机的地址;
第三个参数,指向 保存读取数据的缓存区 的指针;
第四个参数,接收数据的长度(1个长度一个字节)
ret_code_t nrf_drv_twi_rx(nrf_drv_twi_t const * p_instance,
uint8_t address,
uint8_t * p_data,
uint8_t length)
2、nRF52xx TWI 使用示例
今天我们直接使用 TWI库函数移植一下 STH21 温湿度传感器的代码,熟悉一下库函数的使用,以前在使用STM32的时候,常用的都是软件 I2C,就是用IO口模拟的I2C接口,当然如果你能够实现软件 I2C,就说明对 I2C 的原理比较熟悉了
SHT21 程序移植示例
使用硬件 I2C 在库函数中都有提供好的函数,还是比较简单的,我们现在分析一下以前的 I2C 读取程序( I2C的驱动部分就不分析了):
/* --------------------
// measure temperature
1、发送一个读取 温度 指令 cmd 给SHT21
2、等待传感器读取数据
3、读取传感器的数据 ,3个字节数据
*/
i2c_start(); // send start sequence (S)
u8Ack = i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_WRITE);//a write to slave 0x40
u8Ack = (u8Ack<<1)|i2c_write(SHT2X_CMD_MEAS_T); //request to measure temperature
i2c_stop();
delay_ms(SHT2X_TEMP_MEAS_TIME);
i2c_start(); // send start sequence (SR)
u8Ack = i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_READ); // a read from slave 0x40
if(u8Ack==I2C_ACK) {
aTemperature.raw = i2c_read(I2C_ACK)<<8; // read hi byte
aTemperature.raw |= i2c_read(I2C_ACK); // read lo byte
aTemperature.crc = i2c_read(I2C_NACK); // read check sum and finish transfere
}else {
aTemperature.raw = 0;
}
i2c_stop();
温度读取函数的步骤可以分为3步:
- 发送一个读取 温度 指令 cmd 给SHT21,那么我们在 nRF52832 上面使用库函数怎么实现,使用
nrf_drv_twi_tx
函数,刚开始的时候,我通过 mpu6050 的驱动修改,发现问题,后面我们会分析什么问题,使用这个函数只需要一句话:
err_code = nrf_drv_twi_tx(&sht_twi, SHT2X_SLAVEADDRESS, tx_buf, 1, true);
其中 tx_buf 就是需要发送的 cmd 命令; - 等待传感器读取数据
- 读取传感器的数据 ,3个字节数据,读取的话我们使用
nrf_drv_twi_rx
函数,读取3个字节:err_code = nrf_drv_twi_rx(&sht_twi, SHT2X_SLAVEADDRESS, destination, 3);
其中 destination 是你需要保存的缓冲区的地址;
最后移植成功的的 sht21.c 程序如下:
/****************************************Copyright (c)************************************************
sht21 移植程序
2021/10/5 by qzh
根据手册,sht21读取的时候并不需要指定读取特定寄存器的地址
**---------------------------------------------------------------------------------------------------*/
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include "nrf_drv_twi.h"
#include "sht21.h"
struct {
sint16 value;
uint16 raw;
uint8 crc;
} aTemperature, aHumidity;
//TWI驱动程序实例ID,ID和外设编号对应,0:TWI0 1:TWI1
#define TWI_INSTANCE_ID 0
//TWI传输完成标志
static volatile bool m_xfer_done = false;
//定义TWI驱动程序实例,名称为sht_twi
static const nrf_drv_twi_t sht_twi = NRF_DRV_TWI_INSTANCE(TWI_INSTANCE_ID);
//TWI事件处理函数
void sht_twi_handler(nrf_drv_twi_evt_t const * p_event, void * p_context)
{
//判断TWI事件类型
switch (p_event->type)
{
//传输完成事件
case NRF_DRV_TWI_EVT_DONE:
m_xfer_done = true;//置位传输完成标志
break;
default:
break;
}
}
//TWI初始化
void twi_sht21_init(void)
{
ret_code_t err_code;
const nrf_drv_twi_config_t twi_config = {
.scl = SHT_SCL, //定义TWI SCL引脚
.sda = SHT_SDA, //定义TWI SDA引脚
.frequency = NRF_DRV_TWI_FREQ_100K, //TWI速率
.interrupt_priority = APP_IRQ_PRIORITY_HIGH, //TWI优先级
.clear_bus_init = false//初始化期间不发送9个SCL时钟
};
err_code = nrf_drv_twi_init(&sht_twi, &twi_config, sht_twi_handler, NULL);
//检查返回的错误代码
APP_ERROR_CHECK(err_code);
//使能TWI
nrf_drv_twi_enable(&sht_twi);
printf("twi_master_init ok.\r\n");
}
/*
在网上参考的I2C设备扫描程序
如果在非阻塞模式下面(目前Demo使用的模式),这个函数直接用会有点问题,出一些错误
如果加上while (m_xfer_done == false){},会“卡死”,出不去
****
如果使用阻塞模式,下面这个行数能够在正常执行,能够获取到 I2C 设备的地址
所以一般使用建议使用阻塞模式
****
*/
void iic_scan_address(void)
{
ret_code_t err_code;
uint8_t address;
uint8_t sample_data;
bool detected_device = false;
printf("TWI scanner started.\r\n");
twi_sht21_init();//twi init
nrf_delay_ms(2000);
for (address = 1; address <= 127; address++)
{
err_code = nrf_drv_twi_rx(&sht_twi, address, &sample_data, sizeof(sample_data));
if (err_code == NRF_SUCCESS)
{
detected_device = true;
printf("TWI-i2c device detected at address 0x%x.\r\n", address);
}
}
if (!detected_device)
{
printf("No device was found.\r\n");
}
printf("TWI device scan ended.\r\n");
}
// fT = 175.72*u16T/65536.0 - 46.85;
// fRH = 125.0*u16RH/65536.0 - 6.0;
// -------------------------------------------------------------------
sint16 sht21_calcRH(uint16 u16RH)
{
sint16 humidityRH; // variable for result
u16RH &= ~0x0003; // clear bits [1..0] (status bits)
//-- calculate relative humidity [%RH] --
humidityRH = (sint16)(-600 + (12500*(sint32)u16RH)/65536 ); // RH = -6 + 125 * SRH/2^16
return humidityRH; // Return RH*100
}
sint16 sht21_calcTemperature(uint16 u16T)
{
sint16 temperature; // variable for result
u16T &= ~0x0003; // clear bits [1..0] (status bits)
//-- calculate temperature [癈] --
temperature= (sint16)(-4685 + (17572*(sint32)u16T)/65536); //T = -46.85 + 175.72 * ST/2^16
return temperature; //return T*100
}
/*
读取函数,使用库函数,I2C_start 和 ack 会自动处理
注意sht21 的手册, 只需要对 设备地址 写数据 读数据,没有额外特定的寄存器
*/
//bool sht_write(uint8_t register_address, uint8_t value) //这个函数使用需要在需要读取特定的寄存器
bool sht_write(uint8_t cmd)
{
ret_code_t err_code;
uint8_t tx_buf[1];
//准备写入的数据
// tx_buf[0] = register_address;
// tx_buf[1] = value;
tx_buf[0] = cmd;
m_xfer_done = false;
err_code = nrf_drv_twi_tx(&sht_twi, SHT2X_SLAVEADDRESS, tx_buf, 1, true);
while (m_xfer_done == false){}
if (NRF_SUCCESS != err_code)
{
return false;
}
return true;
}
/*
读取函数,
注意SHT21,等待时间直接读取就可以
现在的很多I2C传感器作为从设备,读取的步骤:
1、主机 先写一个 寄存器 地址发送给从设备,所以在读函数里面还是得先调用一次写;
2、然后 主机再 去按照读取的数据大小读取
SHT21 不需要!! 注意代码中注释掉的部分
*/
//bool sht_read(uint8_t register_address, uint8_t * destination, uint8_t number_of_bytes)
bool sht_read(uint8_t * destination, uint8_t number_of_bytes)
{
ret_code_t err_code;
//TWI传输完成标志设置为false
// m_xfer_done = false;
// err_code = nrf_drv_twi_tx(&sht_twi, SHT2X_SLAVEADDRESS, ®ister_address, 1, true);
// //等待TWI总线传输完成
// while (m_xfer_done == false){}
// if (NRF_SUCCESS != err_code)
// {
// return false;
// }
//TWI传输完成标志设置为false
m_xfer_done = false;
err_code = nrf_drv_twi_rx(&sht_twi, SHT2X_SLAVEADDRESS, destination, number_of_bytes);
//等待TWI总线传输完成
while (m_xfer_done == false){}
if (NRF_SUCCESS != err_code)
{
return false;
}
return true;
}
void SHT2X_THMeasure() {
uint8 t_value[3];
uint8 h_value[3];
#if (SHT2X_RESOLUTION != 0x00) // only needed if used resolution other than default
i2c_start(); // send start sequence (S)
u8Ack = i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_WRITE); // write to slave 0x40
u8Ack = (u8Ack<<1)|i2c_write(SHT2X_CMD_RD_REG); // request to read from user register
i2c_start(); // send start sequence (S)
u8Ack = (u8Ack<<1)|i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_READ); // read from slave 0x40
u8UserReg = i2c_read(I2C_NACK); // read user register
i2c_start(); // send start sequence (S)
u8Ack = i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_WRITE); // write to slave 0x40
u8Ack = (u8Ack<<1)|i2c_write(SHT2X_CMD_WR_REG); // request to write user register
u8Ack = (u8Ack<<1)|i2c_write(SHT2X_RESOLUTION | (u8UserReg & ~0x81)); // write new user register data
#endif//(SHT2X_RESOLUTION != 0x00)
// --------------------
// measure temperature
// --------------------
// i2c_start(); // send start sequence (S)
// u8Ack = i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_WRITE);//a write to slave 0x40
// u8Ack = (u8Ack<<1)|i2c_write(SHT2X_CMD_MEAS_T); //request to measure temperature
// i2c_stop();
//sht_write(SHT2X_SLAVEADDRESS,SHT2X_CMD_MEAS_T); //发送一条数据就可以等待测量
sht_write(SHT2X_CMD_MEAS_T);
nrf_delay_ms(SHT2X_TEMP_MEAS_TIME);
// delay_ms(SHT2X_TEMP_MEAS_TIME);
//time_wait(SHT2X_TEMP_MEAS_TIME);
// i2c_start(); // send start sequence (SR)
// u8Ack = i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_READ); //a read from slave 0x40
// if(u8Ack==I2C_ACK) {
// aTemperature.raw = i2c_read(I2C_ACK)<<8; // read hi byte
// aTemperature.raw |= i2c_read(I2C_ACK); // read lo byte
// aTemperature.crc = i2c_read(I2C_NACK); // read check sum and finish transfere
// }else {
// aTemperature.raw = 0;
// }
// i2c_stop();
// sht_read(SHT2X_SLAVEADDRESS,t_value,3);
// nrf_delay_ms(2);
// aTemperature.raw = t_value[0]<<8;
// aTemperature.raw |= t_value[1];
// aTemperature.crc = t_value[2];
if(sht_read(t_value,3)== true){
aTemperature.raw = t_value[0]<<8;
aTemperature.raw |= t_value[1];
aTemperature.crc = t_value[2];
}else{
aTemperature.raw = 0;
}
// -------------------------
// Humidity Measure
// -------------------------
// i2c_start(); // send start sequence (S)
// u8Ack = i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_WRITE); // a write to slave 0x40 1000 0000
// u8Ack = (u8Ack<<1)|i2c_write(SHT2X_CMD_MEAS_RH); // request to measure humidity F5 1110 0101
// i2c_stop();
//sht_write(SHT2X_SLAVEADDRESS,SHT2X_CMD_MEAS_RH); //发送一条数据就可以等待测量
sht_write(SHT2X_CMD_MEAS_RH);
nrf_delay_ms(SHT2X_HUMI_MEAS_TIME);
// shortTermSleep(SHT2X_HUMI_MEAS_TIME);
// delay_ms(SHT2X_HUMI_MEAS_TIME);
// i2c_start(); // send start sequence (SR)
// u8Ack = i2c_write((SHT2X_SLAVEADDRESS<<1)|I2C_READ); // read from slave 0x40 1000 0001
// if(u8Ack==I2C_ACK) { // timeout
// aHumidity.raw = i2c_read(I2C_ACK)<<8; // read hi byte
// aHumidity.raw |= i2c_read(I2C_ACK); // read lo byte
// aHumidity.crc = i2c_read(I2C_NACK); // read check sum and finish transfere
// }else{
// aHumidity.raw = 0;
// }
// i2c_stop();
if(sht_read(h_value,3)== true){
aHumidity.raw = h_value[0] <<8; // read hi byte
aHumidity.raw |= h_value[1]; // read lo byte
aHumidity.crc = h_value[2]; // read check sum and finish transfere
}else{
aTemperature.raw = 0;
}
aTemperature.value = sht21_calcTemperature(aTemperature.raw);
aHumidity.value = sht21_calcRH(aHumidity.raw); // signed value, temperature = aTemperature.value * 0.01?
if(aTemperature.crc!= sht21_CRC((uint8*)&aTemperature.raw, 2)) {}
if(aHumidity.crc!= sht21_CRC((uint8*)&aHumidity.raw, 2)) {}
if(aTemperature.value>5100) aTemperature.value=5100; //prevent temperature over-/underflow
else if(aTemperature.value<0) aTemperature.value=0;
if(aHumidity.value>10000) aTemperature.value=10000; //prevent temperature over-/underflow
else if(aTemperature.value<0) aTemperature.value=0;
}
uint16_t getTemperature() {
return aTemperature.value;
}
uint16_t getHumidity() {
return aHumidity.value;
}
//==============================================================================
uint8 sht21_CRC(uint8 value[], uint8 u8Bytes) {
// CRC
const uint16 POLYNOMIAL = 0x131; //P(x)=x^8+x^5+x^4+1 = 100110001
uint8 crc = 0;
uint8 byteCtr;
uint8 bitCtr;
//calculates 8-Bit checksum with given polynomial
for (byteCtr = 0; byteCtr < u8Bytes; ++byteCtr) {
crc ^= (value[byteCtr]);
for (bitCtr = 8; bitCtr > 0; --bitCtr)
{
if (crc & 0x80) crc = (crc << 1) ^ POLYNOMIAL;
else crc = (crc << 1);
}
}
return crc;
}
额外说明,上面的 TWI 初始化使用了非阻塞模式,如果使用阻塞模式
1、初始化函数变成
err_code = nrf_drv_twi_init(&sht_twi, &twi_config, NULL, NULL);
2、去掉事件处理函数
// void sht_twi_handler(nrf_drv_twi_evt_t const * p_event, void * p_context){...}
3、相应的位置(读写函数中读写数据后)去掉标志位的等待
// while (m_xfer_done == false){}
SHT21 移植问题分析
在 SHT21 移植过程中,开始按照 MPU6050 的程序修改的写函数 和 读函数如下:
bool sht_write(uint8_t register_address, uint8_t value)
bool sht_read(uint8_t register_address, uint8_t * destination, uint8_t number_of_bytes)
然后怎么都出来数据 ……
其实通过 SHT21 以前的程序,不需要指定特定的寄存器写命令和读取数据 ,就能发现和MPU6050的不同
在传感器使用手册中,关于SHT21 使用非主机模式读取 的例子:
我们再来看看 MPU6050 作为从机被写图示:
MPU6050 作为从机读取图示:
通过比较 SHT21 和 MPU6050 读写逻辑,我们可以直观的发现不同之处,所以最后将读写的函数由
bool sht_write(uint8_t register_address, uint8_t value)
bool sht_read(uint8_t register_address, uint8_t * destination, uint8_t number_of_bytes)
改成了
bool sht_write(uint8_t cmd)
bool sht_read(uint8_t * destination, uint8_t number_of_bytes)
具体的实现逻辑可以看上面给出的 sht21.c 程序
最后在主函数中调用
while(true)
{
SHT2X_THMeasure();
T=(float)getTemperature();
H=(float)getHumidity();
T=(getTemperature()/100.0);
H=(getHumidity()/100.0); //OK
printf("\r\n%4.2f C\r\n%4.2f%%\r\n",T,H);
nrf_delay_ms(2000);
}
使用阻塞模式最后的测试效果:
SHT30 程序(待更新)
SHT30 等项目上开始使用的时候,来更新一下代码
MPU6050 程序(待更新)
同上