引言
回顾上节,我们把原理图画完,并且生成了对应的软件代码工程之后,我们需要布局PCB了,以及去编写我们的软件代码。 我们直接进入正题~
正文
这边就是我们布局好的PCB了,上方用type-C接口进行5V供电,然后IIC通信的LCD用于显示脉搏数据,下面放我们的最小系统板以及芯片,传感器。算是一块比较简单的双层板,检查无误后就可以送去刻制,然后导出元器件清单进行元器件的采购。然后硬件部分,就只剩下焊接啦。 下面展示一下成品的图片:
这是已经烧写过代码的实物了,到此我们就完完整整的把硬件部分解决了,下面我们开始软件部分的代码编写。 首先,我们需要知道IIC通信如何用软件去实现。
#include "SOFT_IIC.h"
#define SDA (((GPIOB->IDR)>>13)&0x01)
#define SDA0() ((GPIOB->ODR)&=~((uint16_t)0x01<<13)) //IO口输出低电平
#define SDA1() ((GPIOB->ODR)|=(uint16_t)0x01<<13) //IO口输出高电平
#define SCL0() ((GPIOB->ODR)&=~((uint16_t)0x01<<12)) //IO口输出低电平
#define SCL1() ((GPIOB->ODR)|=(uint16_t)0x01<<12) //IO口输出高电平
#define DIR_OUT() ((GPIOB->CRH)&=(~((uint32_t)0x0f<<20)),(GPIOB->CRH)|=((uint32_t)0x06<<20)) //输出方向
#define DIR_IN() ((GPIOB->CRH)&=(~((uint32_t)0x0f<<20)),(GPIOB->CRH)|=((uint32_t)0x04<<20)) //输入方向
//内部数据定义
uint8 IIC_ad_main; //器件从地址
uint8 IIC_ad_sub; //器件子地址
uint8 *IIC_buf; //发送|接收数据缓冲区
uint8 IIC_num; //发送|接收数据个数
uint8_t soft_iic_unstable_flag=0;//模拟iic不稳定标志,如果至少一次没收到应答信号,则为1
#define ack 1 //主应答
#define no_ack 0 //从应答
//-------------------------------------------------------------------------------------------------------------------
// @brief 模拟IIC延时
// @return void
// @since v1.0
//-------------------------------------------------------------------------------------------------------------------
void simiic_delay(void)
{
volatile uint8_t j;
j=5;
while(--j); //f401 j=10 392.6khz (IAR optimize:Balance)
}
//内部使用,用户无需调用
void IIC_start(void)
{
SDA1();
SCL1();
simiic_delay();
SDA0();
simiic_delay();
SCL0();
}
//内部使用,用户无需调用
void IIC_stop(void)
{
SDA0();
SCL0();
simiic_delay();
SCL1();
simiic_delay();
SDA1();
simiic_delay();
}
//主应答(包含ack:SDA=0和no_ack:SDA=0)
//内部使用,用户无需调用
void I2C_SendACK(unsigned char ack_dat)
{
SCL0();
simiic_delay();
if(ack_dat) SDA0();
else SDA1();
SCL1();
simiic_delay();
SCL0();
simiic_delay();
}
static int SCCB_WaitAck(void)
{
SCL0();
DIR_IN();
simiic_delay();
SCL1();
simiic_delay();
if(SDA) //应答为高电平,异常,通信失败
{
DIR_OUT();
SCL0();
soft_iic_unstable_flag=1;
return 0;
}
DIR_OUT();
SCL0();
simiic_delay();
return 1;
}
//字节发送程序
//发送c(可以是数据也可是地址),送完后接收从应答
//不考虑从应答位
//内部使用,用户无需调用
uint8_t send_ch(uint8 c)
{
uint8 i = 8;
while(i--)
{
if(c & 0x80) SDA1();//SDA 输出数据
else SDA0();
c <<= 1;
simiic_delay();
SCL1(); //SCL 拉高,采集信号
simiic_delay();
SCL0(); //SCL 时钟线拉低
}
return (uint8_t)SCCB_WaitAck();
}
//字节接收程序
//接收器件传来的数据,此程序应配合|主应答函数|使用
//内部使用,用户无需调用
uint8 read_ch(uint8 ack_x)
{
uint8 i;
uint8 c;
c=0;
SCL0();
simiic_delay();
SDA1(); //置数据线为输入方式
DIR_IN();
for(i=0;i<8;i++)
{
SCL0(); //置时钟线为低,准备接收数据位
simiic_delay();
SCL1(); //置时钟线为高,使数据线上数据有效
simiic_delay();
c<<=1;
if(SDA) c+=1; //读数据位,将接收的数据存c
}
DIR_OUT();
SCL0();
simiic_delay();
I2C_SendACK(ack_x);
return c;
}
//-------------------------------------------------------------------------------------------------------------------
// @brief 模拟IIC写数据到设备寄存器函数
// @param dev_add 设备地址(低七位地址)
// @param reg 寄存器地址
// @param dat 写入的数据
// @return void
// @since v1.0
// Sample usage:
//-------------------------------------------------------------------------------------------------------------------
void simiic_write_reg(uint8 dev_add, uint8 reg, uint8 dat)
{
IIC_start();
send_ch( (dev_add<<1) | 0x00); //发送器件地址加写位
send_ch( reg ); //发送从机寄存器地址
send_ch( dat ); //发送需要写入的数据
IIC_stop();
}
//-------------------------------------------------------------------------------------------------------------------
// @brief 模拟IIC从设备寄存器读取数据
// @param dev_add 设备地址(低七位地址)
// @param reg 寄存器地址
// @param type 选择通信方式是IIC 还是 SCCB
// @return uint8 返回寄存器的数据
// @since v1.0
// Sample usage:
//-------------------------------------------------------------------------------------------------------------------
uint8 simiic_read_reg(uint8 dev_add, uint8 reg, IIC_type type)
{
uint8 dat;
IIC_start();
send_ch( (dev_add<<1) | 0x00); //发送器件地址加写位
send_ch( reg ); //发送从机寄存器地址
if(type == SCCB)IIC_stop();
IIC_start();
send_ch( (dev_add<<1) | 0x01); //发送器件地址加读位
dat = read_ch(no_ack); //读取数据
IIC_stop();
return dat;
}
//-------------------------------------------------------------------------------------------------------------------
// @brief 模拟IIC读取多字节数据
// @param dev_add 设备地址(低七位地址)
// @param reg 寄存器地址
// @param dat_add 数据保存的地址指针
// @param num 读取字节数量
// @param type 选择通信方式是IIC 还是 SCCB
// @return uint8 返回寄存器的数据
// @since v1.0
// Sample usage:
//-------------------------------------------------------------------------------------------------------------------
void simiic_read_regs(uint8 dev_add, uint8 reg, uint8 *dat_add, uint8 num, IIC_type type)
{
IIC_start();
send_ch( (dev_add<<1) | 0x00); //发送器件地址加写位
send_ch( reg ); //发送从机寄存器地址
if(type == SCCB)IIC_stop();
IIC_start();
send_ch( (dev_add<<1) | 0x01); //发送器件地址加读位
while(--num)
{
*dat_add = read_ch(ack); //读取数据
dat_add++;
}
*dat_add = read_ch(no_ack); //读取数据
IIC_stop();
}
//-------------------------------------------------------------------------------------------------------------------
// @brief 模拟IIC端口初始化
// @param NULL
// @return void
// @since v1.0
// Sample usage:
//-------------------------------------------------------------------------------------------------------------------
void IIC_init(void)
{
DIR_OUT();
simiic_delay();
SCL0();
simiic_delay();
SDA0();
simiic_delay();
//停止信号复位各个芯片的总线
SCL1();
simiic_delay();
SDA1();
simiic_delay();
}
//扫描所有IIC设备,返回设备总数,设备地址
uint8_t IIC_scan(uint8_t device_addr[128])
{
uint8_t counter=0;
uint8_t i;
uint8_t stable_flag;
stable_flag=soft_iic_unstable_flag;
for(i=0x00;i<=0x7f;i++)
{
IIC_start();
simiic_delay();
if(send_ch( (i<<1) | 0x00))
{
if(device_addr!=NULL) device_addr[counter]=i;
counter++;
}
simiic_delay();
IIC_stop();
simiic_delay();
simiic_delay();
simiic_delay();
simiic_delay();
simiic_delay();
}
soft_iic_unstable_flag=stable_flag;
return counter;
}
这边我很想甩上一段代码就略过,但是不行,毕竟像SPI,IIC或者串口这类通信方式在单片机与传感器的数据交互中应用的比较广泛,因此我们还是有必要对IIC进行一些粗略的讲解。 我们工程里用的是软件IIC,那么什么是硬件IIC呢,所谓的硬件IIC对应芯片上的IIC外设,是需要相应的驱动电路的,其所对应的管脚也是专用的,而软件IIC只需要两个GPIO口就行,硬件IIC的效率是远高于软件IIC的,但是软件IIC不受管脚的限制。软件IIC的实现思路大致就是采用单片机的定时器来模拟IIC通信的时序波形。 IIC总线总共具有两条双向信号线,一条是数据线SDA,另一个是时钟线SCL,在数据传送时需要严格的时序控制,这样才能让单片机获取到传感器的数据。
初学者在了解了IIC之后并不一定要急于去实现,可以根据现有的代码进行阅读并理解,通过已有的库去实现IIC通信,这样就能运用自如了。 我们在按照传感器要求读出对应的数据之后还需要进行一定的换算才能最终得到我们需要的脉搏数,这个过程就是算法编写的过程。当然,在传感器的datasheet中都有介绍如何换算,以及相关公式,这个就并没有什么难度了。 接下来就需要将我们的代码通过烧写器烧入我们的芯片中。 最终的实现效果可以看如下视频:
这边给大家介绍一个工具,类似虚拟示波器的软件,通过烧写器来实时观看芯片运行过程中的变量变化过程,该软件为J-scope
我们可以很清楚的从软件中看到我们的变量波形,这个就是通过算法过滤出来的心率波形了。
总结
这三章,我们通过不太详细的一个讲解实现了我们的心率测量,虽然我讲的很少,但是中间的细节还是很多的,这是通常情况下一个小项目实现的过程。当然,随着大家能力的提高,还会需要用到功能更强大的芯片。 总之,在我的经验看来,本科期间的百分之九十的项目,都离不开嵌入式单片机。 如果各位有什么好点子,但是受限于能力,可以说出来让大家一起讨论解决呀~