项目需求:使用PCB代替PLC模块进行数据采集
项目目的:降低成本
模数转换芯片(ADC)CS1237: 通过两线模拟SPI进行通信 通过配置内部寄存器 可进行电压、电流和电阻的数据采集;
数模转换芯片(DAC)AD5422:通过HAL库配置SPI进行通信,通过配置内部寄存器,可进行电压、 电流的输出;
热电偶数字转换器MAX31856:通过HAL库配置SPI进行通信,通过配置内部寄存器,可进行温度的读取。0-1100℃
通信协议:移植freemodbus从机协议使用RS485与PLC进行的数据交互。
菊花链方式:PLC通过访问不同的MODBUS地址进行不同PCB板上所测数据的读取。
优势:涉及逻辑更加简单,不容易出问题。尤其对于工业需求稳定是第一位。
劣势:受限与modbus的地址数量。
级联模式:因为一整个设备所用很多个测量版,但modbus只有15个地址可用,所以可使用级联模式
如果使用级联模式,那么pcb就需要将modbus主机协议也进行移植,每块pcb板需要至少两个rs485,一个作为从机,一个作为主机。
图1 级联模式
需要每块PCB既可以作为从机接收数据又可以作为主机读取数据。
级联模式的好处就在于不会受到MODBUS地址数量的限制,可以将所有的地址全部设置为1,PLC只与第一块PCB进行数据交互,之后的所有数据都由modbus一层一层传递到第一块PCB中进行保存。
但是这种模式的劣势也很明显,软件逻辑或者时间没有处理好的话可能会出现很多问题。
如果可以的话更推荐通过CAN进行通信,
CS1237芯片驱动:
需要两根PIN脚来模拟SPI进行通信,且对于延时需要精确到us,因为这个项目对于芯片的性能要求不高 此处为了省事直接使用硬延时;
封装的延时函数
void bsp_DelayUS(uint32_t us)
{
delay_us(us);
}
void bsp_DelayMS(uint32_t ms)
{
HAL_Delay(ms);
}
void delay_us(uint32_t us)
{
for(int i=0; i<100;i++)
{
for(; us != 0; us--);
}
}
模拟SPI进行通信
#define CS1237_SCL_H HAL_GPIO_WritePin(CS1237_SCL_GPIO_Port, CS1237_SCL_Pin, GPIO_PIN_SET)
#define CS1237_SCL_L HAL_GPIO_WritePin(CS1237_SCL_GPIO_Port, CS1237_SCL_Pin, GPIO_PIN_RESET)
void DATAIN(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
/*Configure GPIO pin : PF1 */
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
}
void DATAOUT(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
/*Configure GPIO pin : PF1 */
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
}
void CS1237_Init_JX(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
__HAL_RCC_GPIOF_CLK_ENABLE();
GPIO_InitStructure.Pin = GPIO_PIN_1;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOF, &GPIO_InitStructure);
GPIO_InitStructure.Pin = GPIO_PIN_2;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOF, &GPIO_InitStructure);
CS1237_SCL_L;
CS1237_SDA_L;
}
配置 CS1237芯片
这里的dat是写入到cs1237内部寄存器的值,根据需求(测量电压,电流,或者是电阻)并根据芯片手册去写入相对应的值
//配置CS1237芯片
void CS1237_Config(unsigned char dat)
{
unsigned char i;
unsigned int count_i=0;//溢出计时器
DATAOUT();
CS1237_SDA_H; //OUT引脚拉高
DATAIN();
CS1237_SCL_L;// 时钟拉低
while(CS1237_SDA_READ==1) //等待CS237准备好
{
bsp_DelayMS(1);
count_i++;
if(count_i > 3000)
{
DATAOUT();
CS1237_SDA_H; // OUT引脚拉高
CS1237_SCL_H;; // CLK引脚拉高
return;//超时,则直接退出程序
}
}
for(i=0;i<29;i++)// 1 - 29
{
CS1237_SCL_H;; // CLK=1;
bsp_DelayUS(1);
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);
}
DATAOUT();
CS1237_SCL_H;;bsp_DelayUS(1);CS1237_SDA_H;CS1237_SCL_L;bsp_DelayUS(1);//30
CS1237_SCL_H;;bsp_DelayUS(1);CS1237_SDA_H;CS1237_SCL_L;bsp_DelayUS(1);//31
CS1237_SCL_H;;bsp_DelayUS(1);CS1237_SDA_L;CS1237_SCL_L;bsp_DelayUS(1);//32
CS1237_SCL_H;;bsp_DelayUS(1);CS1237_SDA_L;CS1237_SCL_L;bsp_DelayUS(1);//33
CS1237_SCL_H;;bsp_DelayUS(1);CS1237_SDA_H;CS1237_SCL_L;bsp_DelayUS(1);//34
CS1237_SCL_H;;bsp_DelayUS(1);CS1237_SDA_L;CS1237_SCL_L;bsp_DelayUS(1);//35
CS1237_SCL_H;;bsp_DelayUS(1);CS1237_SDA_H;CS1237_SCL_L;bsp_DelayUS(1);//36
//37 写入了0x65
CS1237_SCL_H;; // CLK=1;
bsp_DelayUS(1);
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);
for(i=0;i<8;i++)// 38 - 45个脉冲了,写8位数据
{
CS1237_SCL_H;; // CLK=1;
bsp_DelayUS(1);
if(dat&0x80)
CS1237_SDA_H;// OUT = 1
else
CS1237_SDA_L;
dat <<= 1;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);
}
CS1237_SDA_H;// OUT = 1
CS1237_SCL_H;; // CLK=1;
bsp_DelayUS(1);
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);
}
读取芯片的配置数据
这是读取1237内部寄存器的函数 可以写入之后进行读取测试是否写入成功。
unsigned char Read_Config(void)
{
unsigned char i;
unsigned char dat=0;//读取到的数据
unsigned int count_i=0;//溢出计时器
// unsigned char k=0,j=0;//中间变量
DATAOUT();
CS1237_SDA_H; //OUT引脚拉高
DATAIN();
CS1237_SCL_L;//时钟拉低
while(CS1237_SDA_READ==1)//等待芯片准备好数据
{
bsp_DelayMS(1);
count_i++;
if(count_i > 3000)
{
DATAOUT();
CS1237_SCL_H; // CLK=1;
CS1237_SDA_H; // OUT=1;
return 1;//超时,则直接退出程序
}
}
for(i=0;i<29;i++)// 产生第1到29个时钟
{
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;
}
DATAOUT();
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SDA_H;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;// 这是第30个时钟
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SDA_L;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;// 这是第31个时钟
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SDA_H;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;//32
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SDA_L;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;//33
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SDA_H;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;//34
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SDA_H;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;//35
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SDA_L;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;//36
CS1237_SDA_H;
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;//37 写入0x56 即读命令
dat=0;
DATAIN();
for(i=0;i<8;i++)// 第38 - 45个脉冲了,读取数据
{
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;
dat <<= 1;
if(CS1237_SDA_READ==1)
dat++;
}
//第46个脉冲
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;
DATAOUT();
CS1237_SDA_H; //OUT引脚拉高
return dat;
}
读取ADC数据,返回的是一个有符号数据
int32_t Read_CS1237(void)
{
unsigned char i;
uint32_t dat=0;//读取到的数据
unsigned int count_i=0;//溢出计时器
int32_t temp;
DATAOUT();
CS1237_SDA_H; //OUT引脚拉高
DATAIN();
CS1237_SCL_L;//时钟拉低
while(CS1237_SDA_READ==1)//等待芯片准备好数据
{
bsp_DelayMS(1);
count_i++;
if(count_i > 3000)
{
DATAOUT();
CS1237_SCL_H; // CLK=1;
CS1237_SDA_H; // OUT=1;
return 1;//超时,则直接退出程序
}
}
dat=0;
for(i=0;i<24;i++)//获取24位有效转换
{
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
dat <<= 1;
if(CS1237_SDA_READ==1)
dat ++;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;
}
for(i=0;i<3;i++)//接着前面的时钟 再来3个时钟
{
CS1237_SCL_H; // CLK=1;
bsp_DelayUS(1);;
CS1237_SCL_L; // CLK=0;
bsp_DelayUS(1);;
}
DATAOUT();
CS1237_SDA_H; // OUT = 1;
if(dat&0x00800000)// 判断是负数 最高位24位是符号位
{
temp=-(((~dat)&0x007FFFFF) + 1);// 补码变源码
}
else
{
temp=dat; // 正数的补码就是源码
}
return temp;
}
初始化和配置寄存器成功之后 就可以读取ADC的值通过公式换算来计算出你测量的数据。
AD5422驱动:
使用HAL库配置SPI进行通信,通过阅读芯片的手册并研究时序图
AD5422写函数(通过SPI将连续三个字节写入内部寄存器)
参数 add1:是要写的寄存器 根据手册进行填写
data是要写入的数据
static void uint16_to_uint8(uint16_t value, uint8_t* high_byte, uint8_t* low_byte) {
*high_byte = (value >> 8) & 0xFF;
*low_byte = value & 0xFF;
}
static uint16_t AD5422_write(uint8_t add1,uint16_t data)
{
uint8_t high_byte, low_byte;
uint8_t tx_buf[3] = {0};
uint8_t rx_buf[3] = {0};
uint16_to_uint8(data,&high_byte,&low_byte);
tx_buf[0]=add1;
tx_buf[1]=high_byte;
tx_buf[2]=low_byte;
HAL_GPIO_WritePin(AD5422_CS_PORT, AD5422_CS_PIN, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi_ad5422, tx_buf, rx_buf, sizeof(tx_buf), 100);
HAL_GPIO_WritePin(AD5422_CS_PORT, AD5422_CS_PIN, GPIO_PIN_SET);
delay_us(100);
return 0;
}
uint16_to_uint8函数是将16位数据转换为两个八位数据,因为这个对于这个芯片写入这种方式更为简单。
复位函数
复位寄存器为0x56 根据手册,只需要将最低为置为1即可。
static uint16_t AD5422_ret(void)
{
uint8_t tx_buf[3] = {0};
uint8_t rx_buf[3] = {0};
tx_buf[0]=0x56;
tx_buf[1]=0x00;
tx_buf[2]=0x01;
HAL_GPIO_WritePin(AD5422_CS_PORT, AD5422_CS_PIN, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi_ad5422, tx_buf, rx_buf, sizeof(tx_buf), 100);
HAL_GPIO_WritePin(AD5422_CS_PORT, AD5422_CS_PIN, GPIO_PIN_SET);
delay_us(100);
return 0;
}
读函数
这个芯片的读取需要实现一个回读功能, 根据芯片手册的时序图实现这个读函数。
static uint16_t AD5422_Read(uint8_t add1,uint8_t data)
{
uint8_t high_byte, low_byte;
uint8_t tx_buf[3] = {0};
uint8_t rx_buf[3] = {0};
uint8_t tx_buf_ret[3]={0};
tx_buf_ret[0]=0X00;
tx_buf_ret[1]=0X00;
tx_buf_ret[2]=0X00;
uint16_to_uint8(data,&high_byte,&low_byte);
tx_buf[0]=add1;
tx_buf[1]=high_byte;
tx_buf[2]=low_byte;
HAL_GPIO_WritePin(AD5422_CS_PORT, AD5422_CS_PIN, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi_ad5422, tx_buf, rx_buf, sizeof(tx_buf), 100);
HAL_GPIO_WritePin(AD5422_CS_PORT, AD5422_CS_PIN, GPIO_PIN_SET);
delay_us(100);
HAL_GPIO_WritePin(AD5422_CS_PORT, AD5422_CS_PIN, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi_ad5422, tx_buf_ret, rx_buf, sizeof(tx_buf), 100);
HAL_GPIO_WritePin(AD5422_CS_PORT, AD5422_CS_PIN, GPIO_PIN_SET);
printf("%x,%x,%x\n",rx_buf[0],rx_buf[1],rx_buf[2]);
return 0;
}
寄存器的初始化及量程配置
这里我使用了两个5422,只需实现一个即可
init函数配置为0-10v输出或者0-20ma输出其中之一
使用对应的out函数即可进行输出
ad5422_out_0_10v 参数data范围是0-10对应0-10v
d5422_out_0_20ma 参数data范围是0-20对应0-20ma
void ad5422_out_0_10v_init(int addr)
{
if(addr==0)
{
AD5422_ret();
AD5422_write(0X55,0X7000);
AD5422_write(0X55,0X7001);
}
if(addr==1)
{
AD5422_ret_addr2();
AD5422_write_addr2(0X55,0X7000);
AD5422_write_addr2(0X55,0X7001);
}
}
void ad5422_out_0_10v(int addr,float data)
{
uint16_t temp=0;
temp = data*6068.05;
if(addr==0)
{
AD5422_write_addr2(0X01,temp); //对应模拟输出2
}
if(addr==1)
{
AD5422_write(0X01,temp); //对应模拟输出2
}
}
void ad5422_out_0_20ma_init(int addr)
{
if(addr==0)
{
AD5422_ret();
AD5422_write(0X55,0X4000);
AD5422_write(0X55,0X4006);
AD5422_write(0X55,0X5006);
}
if(addr==1)
{
AD5422_ret_addr2();
AD5422_write_addr2(0X55,0X4000);
AD5422_write_addr2(0X55,0X4006);
AD5422_write_addr2(0X55,0X5006);
}
}
void ad5422_out_0_20ma(int addr,float data)
{
uint16_t temp=0;
temp =data/20*65535;
if(addr==0)
AD5422_write(0X01,temp);
if(addr==1)
AD5422_write_addr2(0X01,temp);
}
MAX31856
使用HAL库配置SPI进行通信
写寄存器 和读寄存器函数 reg的值不懂可以看芯片手册有详细介绍
// 写寄存器
void MAX31856_WriteReg(uint8_t reg, uint8_t data) {
reg |= 0x80; // 设置写标志位(根据MAX31856协议)
HAL_GPIO_WritePin(MAX31856_CS_PORT, MAX31856_CS_PIN, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, ®, 1, 100);
HAL_SPI_Transmit(&hspi1, &data, 1, 100);
HAL_GPIO_WritePin(MAX31856_CS_PORT, MAX31856_CS_PIN, GPIO_PIN_SET);
}
// 读寄存器
uint8_t MAX31856_ReadReg(uint8_t reg) {
uint8_t rx_data;
HAL_GPIO_WritePin(MAX31856_CS_PORT, MAX31856_CS_PIN, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, ®, 1, 100);
HAL_SPI_Receive(&hspi1, &rx_data, 1, 100);
HAL_GPIO_WritePin(MAX31856_CS_PORT, MAX31856_CS_PIN, GPIO_PIN_SET);
return rx_data;
}
初始化函数
此芯片也使用了两个,只实现一个即可
void MAX31856_Init(int addr)
{
if(addr==0) //SPI3对应热电偶1
{
MAX31856_WriteReg_ADDR1(0x01, 0x33); // 连续转换模式,50Hz滤波
MAX31856_WriteReg_ADDR1(0x00, 0xD0); // 选择K型热电偶(具体值参考数据手册)
}
if(addr==1) //SPI1对应热电偶2
{
MAX31856_WriteReg(0x01, 0x33); // 连续转换模式,50Hz滤波
MAX31856_WriteReg(0x00, 0xD0); // 选择K型热电偶(具体值参考数据手册)
}
}
读取温度函数
float MAX31856_ReadTemp(void) {
uint8_t temp[3];
uint32_t raw_temp=0;
float tem;
MAX31856_ReadReg(0x0C); //
uint8_t reg = 0x0C;
HAL_GPIO_WritePin(MAX31856_CS_PORT, MAX31856_CS_PIN, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, ®, 1, 100);
HAL_SPI_Receive(&hspi1, temp, 3, 100);
HAL_GPIO_WritePin(MAX31856_CS_PORT, MAX31856_CS_PIN, GPIO_PIN_SET);
raw_temp = (temp[0] << 16) | (temp[1] << 8) | temp[2];
raw_temp >>= 5; //
tem=raw_temp*0.0078125;
return tem; //
}
对于内部不懂的参数,可先了解相关手册后进行使用
原本此项目使用的是级联模式,之后领导要求使用菊花链方式,上层逻辑部分不复杂,这里就不赘述。
对于modbus从机的移植可观看我之前写的博客。