数码之家

标题: 像IO口一样方便的去控制74HC595的输出 [打印本页]

作者: 西点钟    时间: 2019-5-5 21:50
标题: 像IO口一样方便的去控制74HC595的输出


我想,74HC595的驱动网上有大把的例子,我也曾经参考过别人的。
但是,有时候控制起来确实不太方便。

能不能像IO口一样的来控制74HC595的输出了?
驱动LED,驱动数码管,驱动继电器。。。。

答案是有的。

这也是我在项目中总结出来的,不敢独享,
赠人玫瑰,手有余香。

先来看看原理图。
2组2片74HC595级联,可以扩展32个输出口,当然还可以一直级联下去。1组74HC595级联具体可以扩多少个输出口,
没有试过。
为了驱动和控制方便,2片一组足够了。
[attach]173822[/attach]


下面来看看软件部分。

#define ON   1
#define OFF  0


/*
*  定义引脚
*/
/* 时钟信号线引脚定义 */
sbit HC595CLK1  = P0^3;
/* 片选信号线引脚定义 */
sbit HC595RCK1  = P4^3;
/* 数据输入引脚定义 */
sbit HC595DATA1 = P7^7;

/* 时钟信号线引脚定义 */
sbit HC595CLK2  = P7^6;
/* 片选信号线引脚定义 */
sbit HC595RCK2  = P7^5;
/* 数据输入引脚定义 */
sbit HC595DATA2 = P7^4;


/******************************************************
* 函数名称:SendData
* 函数功能:74HC595数据的发送
* 入口参数:unsigned int uiDataOne, unsigned int uiDataTwo
* 出口参数:void
*******************************************************/
void SendData_0_15(unsigned int uiDataOne, unsigned int uiDataTwo)
{

    unsigned int i = 0;

    /* 将片选信号置为低电平 */
    HC595RCK1 = 0;

    /* 输入第一个数据:uiDataOne */
    for (i = 0; i < 8; i++)
    {
        /* 给出脉冲信号,首先将CLK置为0 */
        HC595CLK1 = 0;
        if (0 != (uiDataOne & 0x80))
        {
            HC595DATA1 = 1;
        }
        else
        {
            HC595DATA1 = 0;
        }
        /* 给出脉冲信号,首先将CLK置为1 */
        HC595CLK1 = 1;
        /* 准备第二个数据 */
        uiDataOne = uiDataOne << 1;
    }

    /* 输入第二个数据:uiDataTwo */
    for (i = 0; i < 8; i++)
    {
        /* 给出脉冲信号,首先将CLK置为0 */
        HC595CLK1 = 0;
        if (0 != (uiDataTwo & 0x80))
        {
            HC595DATA1 = 1;
        }
        else
        {
            HC595DATA1 = 0;
        }
        /* 给出脉冲信号,首先将CLK置为1 */
        HC595CLK1 = 1;
        /* 准备第二个数据 */
        uiDataTwo = uiDataTwo << 1;
    }

    /* 将片选信号置为高电平 */
    HC595RCK1 = 1;
}

/******************************************************
* 函数名称:SendData
* 函数功能:74HC595数据的发送
* 入口参数:unsigned int uiDataOne, unsigned int uiDataTwo
* 出口参数:void
*******************************************************/
void SendData_16_31(unsigned int uiDataOne, unsigned int uiDataTwo)
{

    unsigned int i = 0;

    /* 将片选信号置为低电平 */
    HC595RCK2 = 0;

    /* 输入第一个数据:uiDataOne */
    for (i = 0; i < 8; i++)
    {
        /* 给出脉冲信号,首先将CLK置为0 */
        HC595CLK2 = 0;
        if (0 != (uiDataOne & 0x80))
        {
            HC595DATA2 = 1;
        }
        else
        {
            HC595DATA2 = 0;
        }
        /* 给出脉冲信号,首先将CLK置为1 */
        HC595CLK2 = 1;
        /* 准备第二个数据 */
        uiDataOne = uiDataOne << 1;
    }

    /* 输入第二个数据:uiDataTwo */
    for (i = 0; i < 8; i++)
    {
        /* 给出脉冲信号,首先将CLK置为0 */
        HC595CLK2 = 0;
        if (0 != (uiDataTwo & 0x80))
        {
            HC595DATA2 = 1;
        }
        else
        {
            HC595DATA2 = 0;
        }
        /* 给出脉冲信号,首先将CLK置为1 */
        HC595CLK2 = 1;
        /* 准备第二个数据 */
        uiDataTwo = uiDataTwo << 1;
    }

    /* 将片选信号置为高电平 */
    HC595RCK2 = 1;
}


普通IO口模拟SPI,中规中矩。和大多数驱动一样。
重点部分是控制部分。

.h文件中有4个对外函数,如下。
extern void hc595_init( void );
extern void SendData_0_15(unsigned int uiDataOne, unsigned int uiDataTwo);
extern void SendData_16_31(unsigned int uiDataOne, unsigned int uiDataTwo);
extern void HC595_0_31_OutCtr(unsigned char ucNumber,unsigned char ucState);


重点是
void HC595_0_31_OutCtr(unsigned char ucNumber,unsigned char ucState)

因为,我这是扩展32个输出口,所以是单个输出。想控制数码管的可以按照此方法修改。


void HC595_0_31_OutCtr(unsigned char ucNumber,unsigned char ucState)
{
//  datas.u16_Data = uiDat;

    if((ucNumber>=0) && (ucNumber<=15))
    {
        switch(ucNumber)
        {
        case 0:
            datas1.bits.u8_D0 = ucState;
            break;
        case 1:
            datas1.bits.u8_D1 = ucState;
            break;
        case 2:
            datas1.bits.u8_D2 = ucState;
            break;
        case 3:
            datas1.bits.u8_D3 = ucState;
            break;
        case 4:
            datas1.bits.u8_D4 = ucState;
            break;
        case 5:
            datas1.bits.u8_D5 = ucState;
            break;
        case 6:
            datas1.bits.u8_D6 = ucState;
            break;
        case 7:
            datas1.bits.u8_D7 = ucState;
            break;
        case 8:
            datas1.bits.u8_D8 = ucState;
            break;
        case 9:
            datas1.bits.u8_D9 = ucState;
            break;
        case 10:
            datas1.bits.u8_D10 = ucState;
            break;
        case 11:
            datas1.bits.u8_D11 = ucState;
            break;
        case 12:
            datas1.bits.u8_D12 = ucState;
            break;
        case 13:
            datas1.bits.u8_D13 = ucState;
            break;
        case 14:
            datas1.bits.u8_D14 = ucState;
            break;
        case 15:
            datas1.bits.u8_D15 = ucState;
            break;

        default:
            break;
        }
        SendData_0_15(datas1.Bytes.u8_data_L,datas1.Bytes.u8_data_H);
    }
    else if((ucNumber>=16) && (ucNumber <= 31))
    {
        switch(ucNumber)
        {
        case 16:
            datas2.bits.u8_D0 = ucState;
            break;
        case 17:
            datas2.bits.u8_D1 = ucState;
            break;
        case 18:
            datas2.bits.u8_D2 = ucState;
            break;
        case 19:
            datas2.bits.u8_D3 = ucState;
            break;
        case 20:
            datas2.bits.u8_D4 = ucState;
            break;
        case 21:
            datas2.bits.u8_D5 = ucState;
            break;
        case 22:
            datas2.bits.u8_D6 = ucState;
            break;
        case 23:
            datas2.bits.u8_D7 = ucState;
            break;
        case 24:
            datas2.bits.u8_D8 = ucState;
            break;
        case 25:
            datas2.bits.u8_D9 = ucState;
            break;
        case 26:
            datas2.bits.u8_D10 = ucState;
            break;
        case 27:
            datas2.bits.u8_D11 = ucState;
            break;
        case 28:
            datas2.bits.u8_D12 = ucState;
            break;
        case 29:
            datas2.bits.u8_D13 = ucState;
            break;
        case 30:
            datas2.bits.u8_D14 = ucState;
            break;
        case 31:
            datas2.bits.u8_D15 = ucState;
            break;

        default:
            break;
        }
        SendData_16_31(datas2.Bytes.u8_data_L,datas2.Bytes.u8_data_H);
    }
}


32个输出口,像IO口一样,想那个高电平,那个就输出高电平,想那个输出低电平,就低电平。
不会影响其他31个输出口的状态。这才是关键。代码量少,简单易懂,明明白白。

[attach]173865[/attach]

这里还对输出口做了翻转,像IO口一样。
没有用高深的写法,就是想看着简单易懂。


所有文件截图:
[attach]173888[/attach]




作者: 西点钟    时间: 2019-5-5 22:12
一下子就有沙发坐了。

欢迎大家围观:smile:。

请多多指点。
作者: dujq    时间: 2019-5-5 23:12
没看懂,慢慢看,谢谢
作者: zhkrid    时间: 2019-5-5 23:17
西点钟 发表于 2019-5-5 22:12
一下子就有沙发坐了。

欢迎大家围观。

这么一来可以用8脚单片机驱动数码管做时钟了
作者: 38263547    时间: 2019-5-6 00:26
zhkrid 发表于 2019-5-5 23:17
这么一来可以用8脚单片机驱动数码管做时钟了

驱动数码管还是用TM1637这类芯片来的直接:lol:
作者: 西点钟    时间: 2019-5-6 08:47
38263547 发表于 2019-5-6 00:26
驱动数码管还是用TM1637这类芯片来的直接

https://www.mydigit.cn/forum.php ... id=25150&extra=

用于数码管的1651驱动

我的1651的驱动。

595只是可以驱动很多数码管,省IO口。
作者: 西点钟    时间: 2019-5-6 08:47
zhkrid 发表于 2019-5-5 23:17
这么一来可以用8脚单片机驱动数码管做时钟了

可以的。
作者: zhuls    时间: 2019-5-6 10:39
开一个4字节的缓冲,直接刷新数据就OK了,不用这么复杂吧?
void SendData(u32 data)
{
。。。。。。
}:lol:
作者: 西点钟    时间: 2019-5-6 10:42
zhuls 发表于 2019-5-6 10:39
开一个4字节的缓冲,直接刷新数据就OK了,不用这么复杂吧?
void SendData(u32 data)
{

是不是需要判断不会打扰改变其他输出口的变化。
送数据前,是不是要做判断?

作者: zhuls    时间: 2019-5-6 10:59
西点钟 发表于 2019-5-6 10:42
是不是需要判断不会打扰改变其他输出口的变化。
送数据前,是不是要做判断?
...

每次更改控制时,直接改缓冲寄存器的内容,改完就刷新到595,这是开环控制,如果是闭环控制,就比较复杂了,不是读IO就可以了,从安全角度来说,而是要通过对应的电路来读取受控端的状态,再做相应的控制修改。
作者: zxy882266    时间: 2019-5-6 12:03
先膜拜下,写的很相信,对我等新手很实用
作者: 2545889167    时间: 2019-5-6 12:42
如果能接受速度慢的话 还是挺好的封装方式的
作者: 西点钟    时间: 2019-5-6 13:10
这是利用联合体,位变量来实现的。

typedef union _UNION_DATA     // 联合体/共用体
{
  struct DATA_BIT    //  位变量
        {
         unsigned char u8_D8:1;     // 8
         unsigned char u8_D9:1;     // 9
         unsigned char u8_D10:1;    // 10
         unsigned char u8_D11:1;    // 11
         unsigned char u8_D12:1;    // 12
         unsigned char u8_D13:1;    // 13
         unsigned char u8_D14:1;    // 14
         unsigned char u8_D15:1;        // 15

         unsigned char u8_D0:1;    // 0
         unsigned char u8_D1:1;    // 1
         unsigned char u8_D2:1;    // 2
         unsigned char u8_D3:1;    // 3
         unsigned char u8_D4:1;    // 4
         unsigned char u8_D5:1;    // 5
         unsigned char u8_D6:1;    // 6
         unsigned char u8_D7:1;           // 7
  }bits;

  struct DATA_Byte    // 字节变量
  {
    unsigned char  u8_data_H;
        unsigned char  u8_data_L;
  }Bytes;
  
  unsigned int     u16_Data;
         
}Tdatas;

Tdatas datas1;
Tdatas datas2;



位变量,是一个很好的解决方式,只可惜,大学里面提及的太少了。
估计现在都没有了。

但是,在项目开发中还是比较实用的。

附一份 位变量 的使用。
输入,输出都有。

比如,你的IO口零散分布,比如LCD12864 的数据D0~~~D7,接在不同的IO口上,
怎么像操作 单片机端口一样操作数据了? 比如 51中 P1 = 0X5A。
位变量就可以帮你实现!!!



具体,请看附件。

作者: m182892    时间: 2019-5-6 18:08
谢谢分享!
作者: 座机呀    时间: 2019-5-7 22:21
本帖最后由 座机呀 于 2019-5-7 22:37 编辑

OE需要控制一下吧,或者加个RC电路,让595上电慢一点,595上电的时候里面的RAM是乱的,可能会有误动作.楼主的封装方式其实可以参考楼上讲的,把一组595虚拟成2字节的缓冲,然后在底层以一定的频率刷新到硬件.
像你讲的2组可以模拟一个Port,如果函数封装成STM32标准库那样操作IO的方式,那样只需新添加一个Port定义,应用层输出数据到IO口的调用方式将不用变化.
以上,只是建议

作者: lorn丁    时间: 2019-5-8 16:10
有没有人试过用串口输出数据给595
作者: 西点钟    时间: 2019-5-8 16:36
lorn丁 发表于 2019-5-8 16:10
有没有人试过用串口输出数据给595

这个简单呀。
作者: infozx    时间: 2019-5-8 17:24
NB,还挺实用的。
这个方法比TI的TC系列端口扩展便宜多了,但综合起来不一定比天马微TM系列IO扩展芯片便宜,毕竟可以多段LCD还带键盘扫描带输入。
作者: 西点钟    时间: 2019-5-8 17:57
infozx 发表于 2019-5-8 17:24
NB,还挺实用的。
这个方法比TI的TC系列端口扩展便宜多了,但综合起来不一定比天马微TM系列IO扩展芯片便宜 ...

主要是省IO口,这才是关键
作者: oscillator    时间: 2019-5-8 19:16
本帖最后由 oscillator 于 2019-5-9 18:51 编辑
西点钟 发表于 2019-5-6 10:42
是不是需要判断不会打扰改变其他输出口的变化。
送数据前,是不是要做判断?
...

是的,用32bit的变量就行。也不用担心打扰其他输出口的变化,用位运算和移位单独改变某一个位就可以了。

例如,变量名叫OUT。想把第n位置1,只需要:OUT |= 0x01  << n  。然后把OUT发送出去就可以了。

你也没必要写32个函数,比如Q0_toggle到Q32_toggle,只需要用位异或运算,比如需要toggle第n位,只需要 OUT ^= 0x01 << n  。 然后把OUT发送出去。



作者: oscillator    时间: 2019-5-9 19:01
本帖最后由 oscillator 于 2019-5-11 20:08 编辑
西点钟 发表于 2019-5-6 13:10
这是利用联合体,位变量来实现的。

typedef union _UNION_DATA     // 联合体/共用体

https://stackoverflow.com/questions/6043483/why-bit-endianness-is-an-issue-in-bitfields/6044223#6044223

https://stackoverflow.com/questi ... lds/6044223#6044223


Implementation-defined behavior

根据这个网页的说法,C没有规定位变量是怎么摆放的。换句话来说,你按顺序定义16个位变量,这16个位变量并不必须按顺序对应一个字节里的16个位,要是编译器愿意,正着放,反着放,/*打乱放*/,都不违反C规范。

作者: 西点钟    时间: 2019-5-9 19:07
oscillator 发表于 2019-5-9 19:01
https://stackoverflow.com/questions/6043483/why-bit-endianness-is-an-issue-in-bitfields/6044223#60 ...

有大端与小端的区别。大端在前还是小端在前。
作者: love香    时间: 2019-11-27 01:03
都是大神,我就看看,虽然看了也看不懂:loveliness:
作者: 595953427@qq    时间: 2019-11-27 20:16
C51不是有bdata吗?比位域好用。
作者: Mark_sheng    时间: 2020-8-13 15:22
595953427@qq 发表于 2019-11-27 20:16
C51不是有bdata吗?比位域好用。

SendData_0_15(datas1.Bytes.u8_data_L,datas1.Bytes.u8_data_H);        u8_data_L和u8_data_H是多少呢
作者: jjbboox    时间: 2020-8-18 13:28
本帖最后由 jjbboox 于 2020-8-18 13:39 编辑

我也来凑热闹
给一个Arduino适用的C++ Class的版本

支持发送各种不同类型的数据。这个是用GPIO口软件模拟输出的。
其实可以写成用硬件SPI驱动的样子,只要把构造函数和send函数改一下就可以了。
或者先写个基类,然后派生出软件模拟和硬件方式实现的多个驱动类。用模板实现不同类型,任意长度的数据一次性输出,不管你串联多少个595都没问题。
这个类太简单,都不需要写cpp,直接在头文件中实现就OK了。
main.cpp是实际的使用方法。

drv_74hc595.h
  1. #ifndef _DRV_74HC595_H_
  2. #define _DRV_74HC595_H_
  3. #include <Arduino.h>

  4. template<class T>
  5. class Drv74HC595 {
  6.     public:
  7.         Drv74HC595(const uint16_t _sda, const uint16_t _sck, const uint16_t _push, const uint16_t _en) : sda_pin(_sda), sck_pin(_sck), push_pin(_push), en_pin(_en) {
  8.             pinMode(sda_pin, OUTPUT);
  9.             pinMode(sck_pin, OUTPUT);
  10.             pinMode(push_pin, OUTPUT);
  11.             pinMode(en_pin, OUTPUT);
  12.         };
  13.         void send(T* b, uint16_t len=1) {
  14.             uint32_t andWord = 1;
  15.             andWord <<= (sizeof(T) * 8 - 1);
  16.             digitalWrite(push_pin, LOW);
  17.             for(int l = 0; l < len; l++) {
  18.                 T t_b = b[l];
  19.                 for(int i = 0; i < sizeof(T) * 8; i++) {
  20.                     digitalWrite(sck_pin, LOW);
  21.                     digitalWrite(sda_pin, t_b & andWord);
  22.                     t_b <<= 1;
  23.                     digitalWrite(sck_pin, HIGH);
  24.                 }

  25.             }
  26.             digitalWrite(push_pin, HIGH);
  27.         }
  28.     private:
  29.         uint16_t    sda_pin;
  30.         uint16_t    sck_pin;
  31.         uint16_t    push_pin;
  32.         uint16_t    en_pin;
  33. };

  34. #endif // _DRV_74HC595_H_
复制代码


main.cpp
  1. #include <arduino.h>
  2. #include <drv_74hc595.h>

  3. static const uint16_t sda_pin = 10;
  4. static const uint16_t sck_pin = 11;
  5. static const uint16_t push_pin = 12;
  6. static const uint16_t en_pin = 13;

  7. void setup() {
  8.     // 定义一个以uint16_t为数据单位的595对象
  9.     Drv74HC595<uint16_t> hc595(sda_pin, sck_pin, push_pin, en_pin);
  10.     // 定义一个用于发送的数组
  11.     uint16_t data[2] = {0xff00, 0xf0f0};
  12.     // 发送整个数组到74hc595
  13.     hc595.send(data, 2);
  14.     uint16_t a = 0x2345;
  15.     // 发送1个数据到74hc595
  16.     hc595.send(&a);
  17. }

  18. void loop() {

  19. }
复制代码



作者: wo114116    时间: 2021-10-23 19:35
不错不错不错
作者: 慕名而来    时间: 2021-10-23 22:24
lorn丁 发表于 2019-5-8 16:10
有没有人试过用串口输出数据给595

我只会用串口驱动595或164,即使用其他I/O口驱动也是模拟串口输出的,比如两片595联机驱动5位数码管显示的函数:P3.0(RXD)=DS(pin14);P3.1(TXD)=SH(pin11);
for(a=0;a<5;a++)       //5位数码显示
{
  SBUF=SEG7[d[a]];while(!TI);TI=0; //送显示数据并转换成段码显示,此时段码数据进入位控制595
  SBUF=Wei[a];while(!TI);TI=0;  //给相应位数码管送电,此时段码数据从位控595的9脚移入段码控制595,而位码留存在位控595中
  ST=1;//595(引脚12)锁存控制
  ST=0;//锁存脉冲发送后两片595各司其职完成数码管的段、位显示信号的输出。
}



作者: 桃源客    时间: 2022-1-14 00:15
这个封装的确很好,学习,收藏了以后用到的时候直接引用。
作者: 广东梁百万    时间: 2022-10-24 17:24
jjbboox 发表于 2020-8-18 13:28
我也来凑热闹
给一个Arduino适用的C++ Class的版本

你是个接74hc595是不是4条线?




欢迎光临 数码之家 (https://www.mydigit.cn/) Powered by Discuz! X3.4