TPYBoard USB口作为串口使用

是的,TPYBoard的USB口可以作为串口使用,而且这实际上是它的主要功能之一。

简单来说,当你通过USB线将TPYBoard连接到电脑时,它在电脑上会模拟出一个串行通信端口(通常称为CDC设备)。这使得你可以像使用传统的串口(如RS-232)一样,通过USB线与板子进行双向数据通信。

具体功能和工作方式:

  1. REPL交互(主要用途):
    • 这是最常用的功能。你不需要任何额外的硬件(如USB转TTL模块),只需一根USB线。
    • 你可以通过任何串口终端工具(如PuTTY, Tera Term,或Mu编辑器自带的终端)连接到这个端口。
    • 在这个终端里,你可以直接输入Python代码并立即执行,查看结果,就像在电脑上的Python命令行一样。这对于调试、测试代码片段和与板子交互非常方便。
  2. 通用串行数据通信:
    • 你可以在你的MicroPython程序中,使用 machine.UART 模块来配置和使用这个USB虚拟出的串口。
    • 你可以通过它来与电脑上的其他软件(如Python的 pyserial 库、串口调试助手、Processing程序等)进行自定义的数据收发。
    • 例如,你可以让TPYBoard读取传感器数据,然后通过USB串口发送到电脑上进行显示或记录。

如何在代码中使用:

在MicroPython中,你需要使用 machine.UART 来操作这个USB CDC串口。通常,它被定义为UART 0。

python

from machine import UART

# 创建一个UART对象,使用USB CDC端口(通常是UART 0)
# 波特率对于USB CDC来说通常不重要,但习惯上设置为115200
usb_serial = UART(0, 115200)

# 发送数据到电脑
usb_serial.write("Hello, PC!\n")

# 检查是否有数据从电脑发送过来
if usb_serial.any():
    data_received = usb_serial.read(10) # 读取10个字节
    print("Received:", data_received)

在电脑端如何连接:

  1. 用USB线连接TPYBoard和电脑。
  2. 电脑会自动安装驱动程序(首次使用时可能需要联网)。
  3. 安装成功后,在设备管理器中会看到一个COM端口(Windows)或 /dev/ttyACM0 / /dev/ttyUSB0(Linux/macOS)。
  4. 打开你的串口终端软件,选择对应的端口,设置波特率(通常为115200),然后就可以进行通信了。

注意事项:

  • 波特率:对于USB CDC,实际的通信速度是USB的全速(12 Mbps),波特率设置更多是出于与传统串口工具的兼容性考虑。通常设置为115200,但很多情况下其他速率也能工作。
  • REPL冲突:如果你通过串口向板子发送数据,但板子上的程序没有在读取这个串口,那么你输入的内容会被REPL解释器接收。如果你的程序正在持续地从串口读取数据,可能会与REPL发生冲突。在设计通信协议时需要考虑到这一点。
  • 两个串口:请注意,TPYBoard通常有两个串口资源:
    • UART(0): USB CDC虚拟串口。
    • UART(1): 硬件UART,连接在GPIO引脚(TX, RX)上,用于连接其他串口设备(如GPS、蓝牙模块等)。

总结:

TPYBoard的USB口最主要、最核心的用途之一就是作为一个便捷的串口通道,用于程序开发、调试和与上位机通信。你完全可以把它当作一个即插即用的串口来使用。

repl可以关掉吗?这样就不会冲突了?

是的,REPL完全可以关掉! 这是一个非常正确的思路,可以有效避免串口通信时的冲突。

关闭REPL的方法

方法1:在boot.py中禁用REPL(推荐)

创建一个 boot.py 文件,内容如下:

python

# boot.py - 在启动时运行
import os
import machine

# 方法A:完全禁用REPL over USB
os.dupterm(None)

# 方法B:更彻底的方法 - 取消USB CDC的REPL
# 这需要根据具体的TPYBoard型号调整
# 对于大多数基于STM32的板子:
# from pyb import USB_VCP
# usb = USB_VCP()
# usb.setinterrupt(-1)

方法2:使用uos.dupterm(None)

在代码中动态禁用:

python

import os
import machine

# 禁用REPL
os.dupterm(None)

# 现在可以安全地使用USB串口进行通信
usb_serial = machine.UART(0, 115200)

while True:
    if usb_serial.any():
        data = usb_serial.read()
        # 处理数据...
        usb_serial.write("Echo: " + data)

方法3:使用USB_VCP(针对Pyboard/TPYBoard)

python

from pyb import USB_VCP

usb = USB_VCP()
# 禁用REPL中断
usb.setinterrupt(-1)

# 现在USB端口完全用于数据通信

重新启用REPL的方法

如果关闭了REPL,你需要通过其他方式恢复:

  1. 通过硬件复位:通常重新上电即可恢复
  2. 通过DFU模式:有些板子支持通过DFU模式重新刷写固件
  3. 通过其他接口:如果有WiFi或蓝牙,可以通过WebREPL或蓝牙REPL访问

更好的解决方案:软重启时处理

为了避免永久禁用REPL,可以在每次程序启动时处理:

python

import os
import machine
import sys

# 检查是否是通过REPL启动的
if not sys.stdin.isatty():
    os.dupterm(None)
    print("REPL disabled for serial communication")

实际应用示例

python

import os
import machine
import time

# 启动时禁用REPL
os.dupterm(None)

usb = machine.UART(0, 115200)

def handle_serial_data():
    if usb.any():
        command = usb.read().decode().strip()
        if command == "get_temp":
            # 读取温度传感器等操作
            temp = read_temperature()
            usb.write(f"Temperature: {temp}\n")
        elif command == "restart":
            machine.reset()

while True:
    handle_serial_data()
    time.sleep(0.1)

重要提醒

  1. 备份重要代码:禁用REPL前确保代码已备份
  2. 测试通信:先确保串口通信正常工作再禁用REPL
  3. 保留恢复方法:知道如何重新启用REPL
  4. 考虑使用WebREPL:作为后备访问方式

总结:关闭REPL确实是解决串口冲突的有效方法,特别适合在产品部署或需要稳定串口通信的场景中使用。建议先在开发阶段测试好通信逻辑,再考虑禁用REPL。

MicroPython TPYBoard 添加自定义类库

开始之前,首先要感谢一下TPYBoard技术交流群(157816561)-云之初晓网友的分享。

 将自己编写的一些Python脚本的类库添加到固件中,在使用时只需import调用,就像使用内置的pyb一样简单便捷;同时还有另外一个重要的功能,就是可以很好的保护源代码。比起生成mpy加密文件这种方式来说,显然添加到固件中会更加安全可靠。

搭建micropython编译环境

1.安装arm-none-eabi-gcc交叉编译工具和gcc编译器。

sudo apt-get install gcc-arm-none-eabi
sudo apt-get install git make gcc

2.下载micropython源码包到本地,我下载到了home目录下。

sudo git clone --recursive https://github.com/micropython/micropython.git

MicroPython的源码结构

1.png
docs/micropython的说明文档(restructuredText)
drivers/一些外接设备的实现例程(eg:ds18b20,nrf24l01)
examples/micropython的使用例程
extmod/C语言实现的模块
lib/各类芯片的lib文件
mpy-cross/自带的交叉编译器,可以将.py生成.mpy加密文件
ports/移植到各类MCU上的源码(eg:stm32,esp8266)
tests/测试框架和Python脚本
tools/工具

进入ports目录下你会发现,micropython根据不同的MCU运行平台进行了分类,比如esp8266就是运行在esp8266-WIFI模块上的micropython,stm32是运行在stm32上的,还有cc3200 。

2.png

然后进入stm32/boards目录下,里面又根据stm32不同的系列进行了划分。

添加自定义Py类库

3.回到stm32/目录下,将需要添加到固件中的Python脚本类库放到modules/目录下。编译之前,请确保程序运行无误。为了测试新建了一个test.py文件,简单写了两个函数。如下:

importpyb     defon():      pyb.LED(4).on()defoff():      pyb.LED(4).off()

4.执行编译命令,等待编译完成。

sudo make BOARD=PYBV10

BOARD参数为stm32/boards/目录下相应的开发板名称。

本次教程用的是TPYBoardv102(蓝色)开发板,兼容PYBV10,所以选择PYBV10开发板编译。

若是TPYBoardv102(绿色)基础板,同样选择PYBV10。

若是TPYBoardv102(黑色)开发板,需选择PYBV11。

3.png

生成的固件在stm32/build-PYBV10/目录下的firmware.dfu和firmware.hex文件。

4.png

5.将dfu和hex文件拷贝到本地,进行烧写。

dfu文件的烧写教程,请参考:

http://tpyboard.com/support/reference11/302.html

hex文件的烧写需要借助ST-LINK工具,请参考:http://tpyboard.com/support/reference11/239.html

6.烧写完毕后进行测试,在main.py文件输入以下内容:

importpybimporttesttest.on()pyb.delay(1500)test.off()

ChaCha20在单片机上的实现指南

ChaCha20是一种高效且安全的流密码算法,非常适合在资源受限的单片机(包括STC系列)上使用。下面详细介绍实现方法。

1. 基础实现原理

ChaCha20的核心是通过一个256位密钥和96位nonce(随机数)生成密钥流,然后与明文进行异或运算:

密钥流 = ChaCha20(密钥, nonce, 计数器)
密文 = 明文 ⊕ 密钥流

2. 核心算法实现

以下是适用于单片机的简化ChaCha20实现:

#include <stdint.h>
#include <string.h>

// ChaCha20常量
static const uint32_t cha_cha_const[4] = {
    0x61707865, 0x3320646e, 0x79622d32, 0x6b206574
};

// Quarter Round操作 (核心运算)
#define ROTL32(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
#define QR(a, b, c, d) \
    a += b; d ^= a; d = ROTL32(d, 16); \
    c += d; b ^= c; b = ROTL32(b, 12); \
    a += b; d ^= a; d = ROTL32(d, 8);  \
    c += d; b ^= c; b = ROTL32(b, 7);

// ChaCha20块函数
static void chacha20_block(const uint32_t key[8], const uint32_t counter[2], 
                          const uint32_t nonce[3], uint32_t output[16]) {
    uint32_t x[16];
    int i;

    // 初始化状态矩阵
    // 常量
    memcpy(x, cha_cha_const, sizeof(cha_cha_const));
    // 密钥
    memcpy(x + 4, key, 8 * sizeof(uint32_t));
    // 计数器
    memcpy(x + 12, counter, 2 * sizeof(uint32_t));
    // Nonce
    memcpy(x + 14, nonce, 3 * sizeof(uint32_t));

    uint32_t working_state[16];
    memcpy(working_state, x, sizeof(x));

    // 20轮操作 (10次双轮)
    for (i = 0; i < 10; i++) {
        // 列轮
        QR(working_state[0], working_state[4], working_state[8],  working_state[12]);
        QR(working_state[1], working_state[5], working_state[9],  working_state[13]);
        QR(working_state[2], working_state[6], working_state[10], working_state[14]);
        QR(working_state[3], working_state[7], working_state[11], working_state[15]);
        // 对角轮
        QR(working_state[0], working_state[5], working_state[10], working_state[15]);
        QR(working_state[1], working_state[6], working_state[11], working_state[12]);
        QR(working_state[2], working_state[7], working_state[8],  working_state[13]);
        QR(working_state[3], working_state[4], working_state[9],  working_state[14]);
    }

    // 加回初始状态
    for (i = 0; i < 16; i++) {
        output[i] = working_state[i] + x[i];
    }
}

3. 完整的加密/解密函数

typedef struct {
    uint32_t key[8];
    uint32_t counter[2];  // 64位计数器
    uint32_t nonce[3];    // 96位随机数
    uint8_t keystream[64];
    uint8_t keystream_pos;
} chacha20_ctx;

// 初始化上下文
void chacha20_init(chacha20_ctx *ctx, const uint8_t key[32], 
                   const uint8_t nonce[12], uint64_t initial_counter) {
    memcpy(ctx->key, key, 32);
    memcpy(ctx->nonce, nonce, 12);
    ctx->counter[0] = (uint32_t)initial_counter;
    ctx->counter[1] = (uint32_t)(initial_counter >> 32);
    ctx->keystream_pos = 64; // 强制重新生成密钥流
}

// 生成密钥流
static void generate_keystream(chacha20_ctx *ctx) {
    chacha20_block(ctx->key, ctx->counter, ctx->nonce, (uint32_t*)ctx->keystream);
    ctx->keystream_pos = 0;

    // 递增计数器
    if (++ctx->counter[0] == 0) {
        ctx->counter[1]++;
    }
}

// 加密/解密数据
void chacha20_crypt(chacha20_ctx *ctx, const uint8_t *input, 
                    uint8_t *output, size_t len) {
    size_t i;

    for (i = 0; i < len; i++) {
        if (ctx->keystream_pos >= 64) {
            generate_keystream(ctx);
        }
        output[i] = input[i] ^ ctx->keystream[ctx->keystream_pos++];
    }
}

4. 串口通信集成示例

// 全局加密上下文
chacha20_ctx uart_enc_ctx;

// 初始化加密通信
void uart_encryption_init(void) {
    // 预共享密钥 (实际应用中应该安全存储)
    uint8_t secret_key[32] = {
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
        0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
        0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F
    };

    // 随机数 (每次通信会话应该不同)
    uint8_t nonce[12] = {0};
    // 在实际应用中应该使用真随机数生成器
    // 这里使用简单示例
    for(int i = 0; i < 12; i++) {
        nonce[i] = i * 11; // 应该替换为真正的随机字节
    }

    chacha20_init(&uart_enc_ctx, secret_key, nonce, 0);
}

// 加密并发送数据
void uart_send_encrypted(const uint8_t *data, size_t len) {
    uint8_t encrypted[256]; // 根据实际需求调整大小
    uint8_t len_byte = (uint8_t)len;

    // 加密长度字节
    chacha20_crypt(&uart_enc_ctx, &len_byte, &len_byte, 1);
    UART_SendByte(len_byte);

    // 加密数据
    chacha20_crypt(&uart_enc_ctx, data, encrypted, len);

    // 发送加密数据
    for(size_t i = 0; i < len; i++) {
        UART_SendByte(encrypted[i]);
    }
}

// 接收并解密数据
size_t uart_receive_decrypted(uint8_t *buffer, size_t max_len) {
    uint8_t encrypted_len;

    // 接收加密的长度字节
    if(!UART_ReceiveByte(&encrypted_len)) {
        return 0;
    }

    // 解密长度
    uint8_t data_len = encrypted_len;
    chacha20_crypt(&uart_enc_ctx, &encrypted_len, &data_len, 1);

    if(data_len > max_len) {
        return 0; // 缓冲区太小
    }

    // 接收加密数据
    for(uint8_t i = 0; i < data_len; i++) {
        uint8_t encrypted_byte;
        if(!UART_ReceiveByte(&encrypted_byte)) {
            return 0;
        }
        buffer[i] = encrypted_byte;
    }

    // 解密数据
    chacha20_crypt(&uart_enc_ctx, buffer, buffer, data_len);

    return data_len;
}

5. 优化建议

内存优化版本(适合资源极其受限的MCU)

// 最小内存占用的ChaCha20实现
void chacha20_minimal(const uint8_t key[32], const uint8_t nonce[12],
                     uint64_t counter, const uint8_t *input, 
                     uint8_t *output, size_t len) {
    uint32_t state[16];
    uint8_t keystream[64];
    size_t bytes_processed = 0;

    while(bytes_processed < len) {
        // 构建状态
        memcpy(state, cha_cha_const, 16);
        memcpy(state + 4, key, 32);
        state[12] = (uint32_t)counter;
        state[13] = (uint32_t)(counter >> 32);
        memcpy(state + 14, nonce, 12);

        // 生成密钥流块
        uint32_t working[16];
        memcpy(working, state, 64);

        // 应用20轮ChaCha
        for(int i = 0; i < 10; i++) {
            QR(working[0], working[4], working[8],  working[12]);
            QR(working[1], working[5], working[9],  working[13]);
            QR(working[2], working[6], working[10], working[14]);
            QR(working[3], working[7], working[11], working[15]);
            QR(working[0], working[5], working[10], working[15]);
            QR(working[1], working[6], working[11], working[12]);
            QR(working[2], working[7], working[8],  working[13]);
            QR(working[3], working[4], working[9],  working[14]);
        }

        // 加回初始状态并转换为字节
        for(int i = 0; i < 16; i++) {
            uint32_t result = working[i] + state[i];
            keystream[i*4]   = (uint8_t)(result);
            keystream[i*4+1] = (uint8_t)(result >> 8);
            keystream[i*4+2] = (uint8_t)(result >> 16);
            keystream[i*4+3] = (uint8_t)(result >> 24);
        }

        // 加密数据
        size_t block_remaining = 64;
        if(len - bytes_processed < 64) {
            block_remaining = len - bytes_processed;
        }

        for(size_t i = 0; i < block_remaining; i++) {
            output[bytes_processed + i] = input[bytes_processed + i] ^ keystream[i];
        }

        bytes_processed += block_remaining;
        counter++;
    }
}

6. 安全注意事项

  1. Nonce管理
  • 每个会话使用不同的nonce
  • 绝对不要重复使用(密钥, nonce)对
  • 可以使用递增计数器或随机数生成器
  1. 密钥安全
  • 使用安全的密钥生成方法
  • 考虑定期更换密钥
  • 不要在代码中硬编码生产密钥
  1. 完整性保护
  • ChaCha20只提供保密性,不提供完整性
  • 考虑结合Poly1305认证(ChaCha20-Poly1305 AEAD)

7. 性能考虑

  • 内存使用:基本实现需要约200字节RAM
  • 计算速度:在48MHz的STC8上,加密1KB数据约需几毫秒
  • 优化技巧
  • 使用查表法优化ROTL32操作
  • 内联关键函数
  • 使用芯片特定的指令集优化

这种实现方案在STC8等增强型8051单片机上是完全可行的,提供了良好的安全性和可接受的性能表现。

在单片机串口通信上任何实现chacha20加密

要实现ChaCha20加密在单片机(如STM32、Arduino等)的串口数据传输中,首先需要理解ChaCha20是一种高效的流加密算法,适用于资源有限的嵌入式系统。它使用256位密钥(或128位)和nonce生成密钥流,然后通过XOR操作加密/解密数据流。串口(UART)传输时,可以在发送数据前加密,在接收端解密。

步骤概述

  1. 准备密钥和nonce:密钥通常为32字节(256位),nonce为12字节(96位)。确保nonce每次传输唯一,以避免重放攻击。
  2. 实现ChaCha20算法:使用C语言实现核心函数。以下是一个标准实现(基于开源库,兼容大多数单片机C编译器,如GCC for ARM或AVR)。
  3. 集成到串口传输:
  • 发送端:加密数据,然后通过UART发送。
  • 接收端:接收数据,然后解密。
  1. 注意事项:
  • 单片机资源有限,选择8轮或12轮ChaCha(nr参数)以优化性能(20轮更安全但更慢)。
  • 需要包含标准头文件如stdint.h、string.h。
  • 测试时,确保字节序为小端(little-endian)。
  • 对于具体单片机(如STM32),使用HAL库或寄存器操作UART;对于Arduino,使用Serial库。

ChaCha20 C实现代码

以下是完整的ChaCha20实现代码(提取自开源实现,支持8/12/20轮)。你可以直接复制到你的项目中,放在chacha.h和chacha.c文件中。 10

chacha.h(头文件):

ifndef _CHACHA_H

define _CHACHA_H

include

include

typedef struct {
uint32_t state[16];
uint8_t keystream[64];
size_t pos;
uint_t nr;
} ChachaContext;

typedef int error_t; // 简单定义错误类型,0为成功,非0为失败

define NO_ERROR 0

define ERROR_INVALID_PARAMETER -1

error_t chachaInit(ChachaContext *context, uint_t nr, const uint8_t *key,
size_t keyLen, const uint8_t *nonce, size_t nonceLen);

void chachaCipher(ChachaContext *context, const uint8_t *input,
uint8_t *output, size_t length);

void chachaDeinit(ChachaContext *context);

define LOAD32LE(p) ( \

((uint32_t)(((uint8_t *)(p))[0]) << 0) | \
((uint32_t)(((uint8_t *)(p))[1]) << 8) | \
((uint32_t)(((uint8_t *)(p))[2]) << 16) | \
((uint32_t)(((uint8_t *)(p))[3]) << 24) \

)

define STORE32LE(p, v) \

{ \
uint32_t x = (v); \
((uint8_t *)(p))[0] = (uint8_t)(x >> 0); \
((uint8_t *)(p))[1] = (uint8_t)(x >> 8); \
((uint8_t *)(p))[2] = (uint8_t)(x >> 16); \
((uint8_t *)(p))[3] = (uint8_t)(x >> 24); \
}

define ROL32(a, n) (((a) << (n)) | ((a) >> (32 – (n))))

define MIN(a, b) (((a) < (b)) ? (a) : (b))

endif

chacha.c(源文件):
`c

include “chacha.h”

include // 用于memset

define QUARTER_ROUND(a, b, c, d) \

{ \
a += b; \
d ^= a; \
d = ROL32(d, 16); \
c += d; \
b ^= c; \
b = ROL32(b, 12); \
a += b; \
d ^= a; \
d = ROL32(d, 8); \
c += d; \
b ^= c; \
b = ROL32(b, 7); \
}

error_t chachaInit(ChachaContext *context, uint_t nr, const uint8_t *key,
size_t keyLen, const uint8_t *nonce, size_t nonceLen)
{
uint32_t *w;

if (context == NULL key == NULL nonce == NULL)
return ERROR_INVALID_PARAMETER;

if (nr != 8 && nr != 12 && nr != 20)
return ERROR_INVALID_PARAMETER;

context->nr = nr;
w = context->state;

if (keyLen == 16)
{
w[0] = 0x61707865;
w[1] = 0x3120646E;
w[2] = 0x79622D36;
w[3] = 0x6B206574;
w[4] = LOAD32LE(key);
w[5] = LOAD32LE(key + 4);
w[6] = LOAD32LE(key + 8);
w[7] = LOAD32LE(key + 12);
w[8] = LOAD32LE(key);
w[9] = LOAD32LE(key + 4);
w[10] = LOAD32LE(key + 8);
w[11] = LOAD32LE(key + 12);
}
else if (keyLen == 32)
{
w[0] = 0x61707865;
w[1] = 0x3320646E;
w[2] = 0x79622D32;
w[3] = 0x6B206574;
w[4] = LOAD32LE(key);
w[5] = LOAD32LE(key + 4);
w[6] = LOAD32LE(key + 8);
w[7] = LOAD32LE(key + 12);
w[8] = LOAD32LE(key + 16);
w[9] = LOAD32LE(key + 20);
w[10] = LOAD32LE(key + 24);
w[11] = LOAD32LE(key + 28);
}
else
{
return ERROR_INVALID_PARAMETER;
}

if (nonceLen == 8)
{
w[12] = 0;
w[13] = 0;
w[14] = LOAD32LE(nonce);
w[15] = LOAD32LE(nonce + 4);
}
else if (nonceLen == 12)
{
w[12] = 0;
w[13] = LOAD32LE(nonce);
w[14] = LOAD32LE(nonce + 4);
w[15] = LOAD32LE(nonce + 8);
}
else if (nonceLen == 16)
{
w[12] = LOAD32LE(nonce);
w[13] = LOAD32LE(nonce + 4);
w[14] = LOAD32LE(nonce + 8);
w[15] = LOAD32LE(nonce + 12);
}
else
{
return ERROR_INVALID_PARAMETER;
}

context->pos = 0;
return NO_ERROR;
}

void chachaCipher(ChachaContext *context, const uint8_t *input,
uint8_t *output, size_t length)
{
uint_t i;
uint_t n;
uint8_t *k;

while (length > 0)
{
if (context->pos == 0 || context->pos >= 64)
{
chachaProcessBlock(context);
context->state[12]++;
if (context->state[12] == 0)
{
context->state[13]++;
}
context->pos = 0;
}

  n = MIN(length, 64 - context->pos);
  if (output != NULL)
  {
     k = context->keystream + context->pos;
     if (input != NULL)
     {
        for (i = 0; i < n; i++)
        {
           output[i] = input[i] ^ k[i];
        }
     }
     else
     {
        for (i = 0; i < n; i++)
        {
           output[i] = k[i];
        }
     }
  }

  context->pos += n;
  length -= n;

}
}

static void chachaProcessBlock(ChachaContext *context)
{
uint_t i;
uint32_t w[16];

for (i = 0; i < 16; i++) { w[i] = context->state[i];
}

for (i = 0; i < context->nr; i += 2)
{
QUARTER_ROUND(w[0], w[4], w[8], w[12]);
QUARTER_ROUND(w[1], w[5], w[9], w[13]);
QUARTER_ROUND(w[2], w[6], w[10], w[14]);
QUARTER_ROUND(w[3], w[7], w[11], w[15]);

  QUARTER_ROUND(w[0], w[5], w[10], w[15]);
  QUARTER_ROUND(w[1], w[6], w[11], w[12]);
  QUARTER_ROUND(w[2], w[7], w[8], w[13]);
  QUARTER_ROUND(w[3], w[4], w[9], w[14]);

}

for (i = 0; i < 16; i++) { w[i] += context->state[i];
}

for (i = 0; i < 16; i++) { STORE32LE(context->keystream + i * 4, w[i]);
}
}

void chachaDeinit(ChachaContext *context)
{
memset(context, 0, sizeof(ChachaContext));
}

示例:集成到单片机串口传输

假设使用STM32单片机(使用HAL库),以下是发送端加密并传输的示例代码。接收端类似,只需调用解密(chachaCipher函数对称)。

c

include “chacha.h”

include “stm32f4xx_hal.h” // 假设STM32F4系列,替换为你的芯片

extern UART_HandleTypeDef huart1; // 你的UART句柄

int main(void) {
ChachaContext context;
uint8_t key[32] = {0x00, 0x01, …}; // 32字节密钥,自行填充
uint8_t nonce[12] = {0x00, 0x00, …}; // 12字节nonce
uint8_t plaintext[100] = “Hello, World!”; // 要发送的数据
uint8_t ciphertext[100];
size_t dataLen = strlen((char*)plaintext);

// 初始化ChaCha
chachaInit(&context, 20, key, 32, nonce, 12);  // 20轮,32字节密钥,12字节nonce

// 加密数据
chachaCipher(&context, plaintext, ciphertext, dataLen);

// 通过串口发送加密数据
HAL_UART_Transmit(&huart1, ciphertext, dataLen, HAL_MAX_DELAY);

// 清理
chachaDeinit(&context);

while(1);

}
`

  • 接收端:类似初始化ChaCha(相同密钥和nonce),接收数据后调用chachaCipher(&context, ciphertext, plaintext, dataLen);解密。
  • Arduino示例:替换HAL为Serial.write(ciphertext, dataLen);

如果你的单片机是其他类型(如8051),需要调整UART函数。测试时,可以先在PC上编译验证,然后移植到单片机。ChaCha20效率高,适合低功耗设备。如果需要Poly1305认证(ChaCha20-Poly1305),可以扩展实现。

串口通信加密备考

在单片机串口传输数据时,加密可以保护数据的机密性和完整性。以下是几种常见的串口数据加密方法,适合单片机资源受限的场景:1. 对称加密对称加密使用同一密钥进行加密和解密,适合单片机因其计算开销较小。

  • 算法选择:
    • AES(高级加密标准):安全性高,适合有硬件加速的单片机(如STM32支持AES硬件模块)。
      • 模式:推荐使用CBC(密码分组链接)或CTR(计数器模式),避免ECB模式(不安全)。
      • 示例(伪代码,使用AES-CBC):c#include <aes.h> uint8_t key[16] = { /* 128位密钥 */ }; uint8_t iv[16] = { /* 初始化向量 */ }; void encrypt_data(uint8_t *data, uint8_t *output, uint32_t len) { aes_cbc_encrypt(data, output, len, key, iv); }
    • XOR加简单密钥:对于资源极低的单片机,可用简单XOR加密(安全性较低,仅防简单窃听)。
      示例:cuint8_t key = 0x55; void encrypt(uint8_t *data, uint8_t *output, uint32_t len) { for (uint32_t i = 0; i < len; i++) { output[i] = data[i] ^ key; } }
  • 优点:计算速度快,适合实时传输。
  • 缺点:密钥分发需安全通道,单片机需存储密钥。

2. 非对称加密非对称加密使用公钥加密、私钥解密,适合密钥交换或认证,但计算复杂,资源占用高。

  • 算法:RSA或ECC(椭圆曲线加密)。ECC更适合单片机,因其密钥长度短、计算开销低。
  • 应用场景:用于初始密钥交换(如Diffie-Hellman协议),后续用对称加密传输数据。
  • 局限性:单片机性能可能不足,需专用加密芯片或高性能MCU。

3. 消息认证码(MAC)为确保数据完整性和防止篡改,可结合加密使用MAC。

  • 算法:HMAC(如HMAC-SHA256)或简单的校验和。
  • 实现:在数据后附加MAC,接收端验证数据完整性。 示例(简单校验):cuint8_t calculate_checksum(uint8_t *data, uint32_t len) { uint8_t checksum = 0; for (uint32_t i = 0; i < len; i++) { checksum ^= data[i]; } return checksum; }

4. 硬件支持一些单片机提供加密硬件模块,极大提升效率:

  • STM32:支持AES、DES、SHA等硬件加速。
  • ESP32:内置AES、SHA、RNG模块。
  • 示例(ESP32 AES加密):c#include <mbedtls/aes.h> void encrypt_data(uint8_t *input, uint8_t *output, size_t len) { mbedtls_aes_context aes; mbedtls_aes_init(&aes); mbedtls_aes_setkey_enc(&aes, key, 128); mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, len, iv, input, output); mbedtls_aes_free(&aes); }

5. 轻量化加密算法为低端单片机设计,资源占用少:

  • ChaCha:轻量、高效,适合8位/16位单片机。
  • Speck/Simon:NSA设计的轻量级加密算法,适合物联网设备。

6. 实现步骤

  1. 选择加密算法:根据单片机性能和安全需求选择(如AES、ChaCha)。
  2. 密钥管理:
    • 预置密钥(存储在单片机的Flash或EEPROM,注意防读取)。
    • 使用Diffie-Hellman或物理方式分发密钥。
  3. 初始化向量(IV):每次加密使用随机IV(通过RNG生成),并随数据传输。
  4. 数据格式:加密数据 + IV + MAC(可选),确保接收端能正确解密和验证。
  5. 传输:通过串口(如UART)发送加密数据,接收端解密并验证。

7. 注意事项

  • 性能:单片机资源有限,避免复杂算法影响实时性。
  • 安全性:避免弱加密(如简单XOR)用于敏感数据。
  • 密钥存储:防止密钥泄露,考虑使用安全芯片(如ATECC608A)。
  • 调试:加密后数据不可读,确保调试工具支持加密协议。
  • 功耗:加密算法可能增加功耗,优化算法和硬件选择。

8. 推荐方案

  • 低端单片机:用XOR或Speck,结合简单校验。
  • 中高端单片机:用AES-CBC(硬件加速)+ HMAC-SHA256。
  • 高安全性场景:结合ECC密钥交换和AES加密。

如需针对具体单片机型号(如STM32、AVR、ESP32)或应用场景的代码示例,请提供更多细节!

关于单片机如何产生随机数

单片机产生随机数通常有以下几种方法,每种方法适用于不同的场景和资源限制:

  1. 伪随机数生成器(PRNG)
    单片机通常使用伪随机数生成算法(如线性同余法、线性反馈移位寄存器等)生成随机数。
    • 线性同余法(LCG):
      公式:X(n+1) = (a * X(n) + c) mod m
      其中,X(n) 是当前随机数,a、c、m 是精心选择的常数。
      实现简单,适合资源受限的单片机,但随机性较弱,周期较短。
      示例代码(C语言):cunsigned int seed = 12345; // 初始种子 unsigned int rand() { seed = (1103515245 * seed + 12345) & 0x7fffffff; // 线性同余法 return seed; }
    • 线性反馈移位寄存器(LFSR):
      使用移位寄存器和异或运算生成伪随机序列,占用资源少,适合硬件实现。
      示例:16位LFSR,反馈多项式为 x^16 + x^14 + x^13 + x^11 + 1。
  2. 利用硬件噪声或外部信号
    单片机可以通过读取不确定的硬件信号生成真随机数(TRNG):
    • ADC噪声:读取ADC(模数转换器)输入端的噪声(如浮空引脚或模拟传感器信号)。
      示例:读取ADC值的最低几位作为随机数种子。
    • 定时器抖动:利用定时器捕获外部信号的时序抖动(如按键抖动或晶振漂移)。
    • 硬件随机数生成器:一些高级单片机(如STM32、ESP32)内置硬件随机数生成器(RNG模块),可直接调用。
      示例(STM32):c#include "stm32f4xx_rng.h" uint32_t get_random() { while (RNG_GetFlagStatus(RNG_FLAG_DRDY) == RESET); // 等待随机数准备好 return RNG_GetRandomNumber(); }
  3. 结合种子初始化
    伪随机数需要一个初始种子,种子决定了序列的起点。常见种子来源:
    • 系统上电时间或计数器值。
    • 用户输入(如按键按下的时间)。
    • 外部传感器数据(如温度、电压)。
    • 实时时钟(RTC)值。
      示例:用毫秒计数作为种子:
    cseed = SysTick->VAL; // 使用系统滴答计数器
  4. 注意事项
    • 随机性质量:伪随机数周期有限,需根据应用选择合适的算法。真随机数更适合密码学等高安全性场景。
    • 资源限制:单片机的存储和计算能力有限,优先选择简单算法(如LFSR)。
    • 初始化种子:避免每次上电使用固定种子,否则随机数序列会重复。
    • 测试与验证:可用统计工具(如Diehard测试)验证随机数质量。

总结:

  • 如果资源有限且要求不高,使用线性同余法或LFSR生成伪随机数。
  • 如果需要高随机性,优先利用硬件RNG或ADC噪声生成真随机数。
  • 确保种子来源多样化,避免序列重复。

如需具体单片机型号的实现方案或代码示例,请提供更多细节!

转再倪海厦的文章

倪師中醫養生

告別血糖焦慮:中醫食療智慧,五種「天然胰島素」幫您找回身體平衡

最新消息 · 2025-08-20

告別血糖焦慮:中醫食療智慧,五種「天然胰島素」幫您找回身體平衡

您是否也曾因為體檢報告上那個小小的紅色箭頭而心驚膽顫?當被告知血糖偏高,需要終身服藥、嚴格忌口時,是否感覺人生瞬間失去了許多樂趣?米飯不敢吃,水果不敢碰,每天吃飯如同上刑場。

但請您靜下來感受一下,在嚴苛的飲食控制下,您的身體真的變好了嗎?還是說,數字稍微好看了,人卻變得面色蠟黃、神疲乏力、手腳冰冷?

在中醫看來,血糖問題的根源,往往在於我們的「脾胃」功能失調了。脾胃是後天之本,負責將食物轉化為身體可用的能量(氣血)。當脾胃這個「運輸隊長」虛弱了,能量便會堆積在體內,反映出來就是血糖升高。

西藥降糖,好比是把倉庫裡堆積的貨物一把火燒掉,數字漂亮了,但身體其他需要能量的地方卻在「嗷嗷待哺」。而中醫的智慧,則是去修復和滋養脾胃這個隊長,讓它自己恢復運輸和分配能量的能力。

今天,就讓我們一起走進廚房,認識五位能幫助您調理脾胃、讓身體回歸平衡的「食療大將」!

一、 清熱除濕的良藥:苦瓜

很多血糖偏高的人,體內常有「濕熱」,會感覺口乾口苦、身體沉重、大便粘膩。濕熱就像一團黏糊糊的膠水,捆住了脾胃。

苦瓜的苦寒之性,正是清除這團濕熱垃圾的清潔工。但要注意,苦瓜性寒,生吃或大量喝汁會損傷脾胃陽氣。

推薦食方:【薑絲豬油炒苦瓜】

做法:鍋中放少許豬油(甘潤,滋養脾陰),加入幾片生薑(辛溫,制衡苦瓜寒性),大火快炒苦瓜。

功效:既能清濕熱,又不傷脾胃根本,一舉兩得。建議一週食用兩三次。

二、 健脾益氣的基石:山藥

如果說血糖問題的根源是脾胃虛弱,那麼山藥就是直接為脾胃「充電」的能量棒。它性情平和,不寒不燥,最能補脾胃之氣。

山藥不僅能補脾,還能益肺、固腎,對中醫「上、中、下消」三個層面的問題都有助益,是調理身體水液和能量代謝的絕佳幫手。

推薦食方:【清蒸山藥泥】

做法:將新鮮的鐵棍山藥蒸熟,搗成泥狀食用。這種吃法極易消化吸收,尤其適合消化能力弱的老人與小孩。

提醒:調理身體如種莊稼,貴在堅持,持之以恆地食用山藥,方能見到效果。

三、 通陽散寒的先鋒:洋蔥

許多血糖偏高者常感到手腳發麻、冰冷,這是因為體內陽氣不足或被寒濕困阻,氣血無法順暢抵達四肢末梢。

洋蔥的辛溫之氣,就像一位開路先鋒,能發散體內的寒濕,打通瘀堵的陽氣通道,同時還能活血化瘀,清潔血管。

推薦食方:【洋蔥炒肉絲】

做法:在日常炒菜或燉湯時加入洋蔥,讓其辛溫之性慢慢釋放。

功效:陽氣通暢,氣血順流,手腳自然回暖,麻木感也會隨之減輕。

四、 補中益氣的親兵:南瓜

一聽到「甜」,許多人便談虎色變。但請放心,南瓜來自大自然的甘甜,是脾胃最喜歡的「本家兄弟」,它溫和滋養,能為虛弱的脾胃補中益氣。

與會產生濕氣、加重脾胃負擔的加工糖不同,南瓜的甘甜是在給脾胃補充能量,讓它更有力氣去運化食物。

推薦食方:【小米南瓜粥】

做法:將南瓜切塊,與小米一同熬煮成粥。

智慧:許多長壽老人都愛吃南瓜、紅薯,這正是老祖宗留下的生活智慧。

五、 化痰軟堅的水將:海帶

對於體型較肥胖、體內「痰濕」較重的患者,海帶是不可多得的清道夫。中醫認為,痰濕會堵塞經絡血管,阻礙氣血運行。

海帶味鹹入腎,能將痰濕瘀血凝結成的硬塊(即「堅」)軟化、打散。

推薦食方:【冬瓜海帶排骨湯】

做法:海帶搭配能利水滲濕的冬瓜一同煲湯。

功效:雙管齊下,掃除體內痰濕水飲,還脾胃一個清爽的環境。對穩定血壓、血脂亦有幫助。

最重要的叮嚀:您才是自己最好的醫生

介紹這五種食物,是希望您能理解其背後的道理,學會「辨證施食」。

傾聽身體的聲音:您是寒性體質還是熱性體質?吃了某樣東西後是舒服還是腹脹?身體的感覺遠比冰冷的數字更真實。

別再妖魔化主食:《黃帝內經》云「五穀為養」,米飯、饅頭是我們幾千年來濡養脾胃的根本。為了控糖而不吃主食,只會讓脾胃越來越虛,陷入惡性循環。請務必正常、均衡地飲食。

養心即是養脾:「思慮傷脾」,憂愁、焦慮會直接損傷脾胃功能。請放寬心,天大的事也要先吃好飯、睡好覺。心情舒暢,脾胃功能才能舒展。

總結

不要再做數字的奴隸。人體有著強大的自癒能力,您要做的,是用溫和、自然的方式去幫助它。把您的廚房變成藥房,將一日三餐視為最好的修行。吃對了,吃好了,身體回報給您的,將不僅僅是穩定的血糖,更是充沛的精力、安穩的睡眠和紅潤的氣色——這,才是我們所追求的真正健康之道。

留言

Xianlin

2025-08-23 00:42

倪师确实是一位让人敬佩的中医传承者,他的讲座使我们从中医小白转变成对他讲座的痴迷。让我对自己改变处处不适有了信心和盼望。也感谢你们辛勤的付出整理出这多的资讯,使广大民众受益。让倪师在天之灵感到辛慰!

李虹萱

2025-08-23 07:07

支持倪師所以視頻

發表留言

稱呼Email(可選)內容送出留言

© 2025 倪師中醫養生

摘要

翻译

扩写

重写

解释说明

语法

问答

解释代码

解释

摘要

翻译

扩写

重写

解释说明

语法

问答

解释代码

解释

HID文档PDF下载大全

 2023-10-25  3891  1

MAX7219级联

Arduino-ESP8266之MAX7219 8位数码管级联的实现 [复制链接]
duocool电梯直达1# 发表于 2018-5-18 16:26:58 | 只看该作者 回帖奖励    先上最终效果,感觉还是可以的,不过要是想完美,得像卖得挺火的那种木壳钟一样,平常像块木头,显示的时候才透出来,不过没找到合适的外壳。
    最近想用arduino-esp8266网上抓点东西下来玩,先试了试OLED模块,网上0.9寸的多,怕小买了1.3寸的,倒是搞定了显示,不过感觉还是小,不够用。

     还有种办法是用电子纸模块,淘宝上也有现成的,不过2,3百的价钱感觉性价比不高。后来决定用下面这种Max7219 8段数码管模块


这种模块从5块多到10多块,样子都差不多,都宣称三线驱动,支持级联,有例程。有焊的也有没焊接口的,我省事买了7块多的焊了的,后的发觉要级联还是得焊,还不如买没焊接口的,自己接线还能紧凑点。

   然后就是填级联这个坑了。最省力的应该是用LedControl这个库了,论坛里也有用这个库max7219 级联LED点阵的例子。不过不管怎么试,都是报错,好象这个库是不支持esp8266的。问卖家要到例程,arduino的例程只有单个模块的,这个样子,没用什么库,感觉就是别的单片机的c代码直接移值过来的。
/*******************************************************************************       
* Software Author:        HQ       
* Creation Date:        2015-2-10
* Software History:        2015-3-10
* Version:              2.0
* Sales address:       http://qifeidz.taobao.com/
********************************************************************************/
//模块引脚定义
int CLK = 2;
int CS = 1;
int DIN = 0; //这里定义了那三个脚

void setup() {
  // put your setup code here, to run once:
  pinMode(CLK,OUTPUT);
  pinMode(CS,OUTPUT);
  pinMode(DIN,OUTPUT); //让三个脚都是输出状态
}

void loop() {
  // put your main code here, to run repeatedly:
   Delay_xms(50);
   Init_MAX7219();
   Delay_xms(2000);
   Write_Max7219(0x0f, 0x00);       //显示测试:1;测试结束,正常显示:0
   Write_Max7219(1,8);
   Write_Max7219(2,7);
   Write_Max7219(3,6);
   Write_Max7219(4,5);
   Write_Max7219(5,4);
   Write_Max7219(6,3);
   Write_Max7219(7,2);
   Write_Max7219(8,1);
   while(1);
}
void Delay_xms(unsigned int x)
{
  unsigned int i,j;
  for(i=0;i<x;i++)
  for(j=0;j<112;j++);
}
//——————————————–
//功能:向MAX7219(U3)写入字节
//入口参数:DATA
//出口参数:无
//说明:
void Write_Max7219_byte(unsigned char DATA)         
{
    unsigned char i;   
    digitalWrite(CS,LOW);               
    for(i=8;i>=1;i–)
    {                  
      digitalWrite(CLK,LOW);   
      if(DATA&0X80)
           digitalWrite(DIN,HIGH);
      else
           digitalWrite(DIN,LOW);
      DATA<<=1;
      digitalWrite(CLK,HIGH);
     }                                 
}
//——————————————-
//功能:向MAX7219写入数据
//入口参数:address、dat
//出口参数:无
//说明:
void Write_Max7219(unsigned char address,unsigned char dat)
{
   digitalWrite(CS,LOW);
   Write_Max7219_byte(address);           //写入地址,即数码管编号
   Write_Max7219_byte(dat);               //写入数据,即数码管显示数字
   digitalWrite(CS,HIGH);                        
}

void Init_MAX7219(void)
{
   Write_Max7219(0x09, 0xff);       //译码方式:BCD码
   Write_Max7219(0x0a, 0x03);       //亮度
   Write_Max7219(0x0b, 0x07);       //扫描界限;4个数码管显示
   Write_Max7219(0x0c, 0x01);       //掉电模式:0,普通模式:1
   Write_Max7219(0x0f, 0x01);       //显示测试:1;测试结束,正常显示:0
}复制代码

试了下,可以用。级联怎么解决呢,网上反复找,本坛里有篇写的http://www.geek-workshop.com/for … p;highlight=max7219控制led点阵的,试着小改用了用,显示还是乱的,不行。又搜到这个贴http://lib.csdn.net/article/embeddeddevelopment/63594 MAX7219多级联串行控制多个点阵/数码管的详解  用的是c语言,都不是写给arduino用的,不过我一看注释那么熟,有些部分简直和卖家给的例程一模一样,可以肯定他们都有一个共同的祖先。仔细研究后,发现级联的关键是写后面的模块就得把它前面的模块写空。像这样/*第二片MAX7219的写入数据*/
void Write_Max7219_2(uchar add2,uchar dat2)
{
        Max7219_pinCS=0;
        Write_Max7219_byte(add2);
        Write_Max7219_byte(dat2);
        Max7219_pinCLK=1;
        Write_Max7219_byte(0x00);  //片1写入空
        Write_Max7219_byte(0x00);
        Max7219_pinCS=1;
}
/*第三片MAX7219的写入数据*/
void Write_Max7219_3(uchar add3,uchar dat3)
{
        Max7219_pinCS=0;
        Write_Max7219_byte(add3);
        Write_Max7219_byte(dat3);
        Max7219_pinCLK=1;
        Write_Max7219_byte(0x00); //片1写入空
        Write_Max7219_byte(0x00);
        Write_Max7219_byte(0x00); //片2写入空
        Write_Max7219_byte(0x00);
        Max7219_pinCS=1;
}
复制代码


我的代码如下: 定义了一个总的片数,然后用循环把初始化数码管模块和写每片的数据都统一起来了,这样不管是写一片,还是8片,代码都是一样的长度(好象max7219最多就支持8片),还自定义了一个DP参数,解决了卖家例程没写的小数点有无的问题。卖家例程是在loop中初始化模块的,这样显示感觉有闪烁,我移到setup中初始化,闪烁就没了
//模块引脚定义
int CLK = D6;
int CS = D7;
int DIN = D8; //这里定义了那三个脚
int PIECENUM = 4;//数码管片数

void setup() {
  // put your setup code here, to run once:
  pinMode(CLK, OUTPUT);
  pinMode(CS, OUTPUT);
  pinMode(DIN, OUTPUT); //让三个脚都是输出状态
  Delay_xms(50);
  Init_MAX7219(PIECENUM);
  Delay_xms(2000);
}

void loop() {
  // put your main code here, to run repeatedly:

  Write_Max7219(1, 0x0f, 0x00, 0);     //显示测试:1;测试结束,正常显示:0

  for (int i = 1; i <= 5; i++) {
    Write_Mynum(1, i, i + 1, 1);
  }
  //Write_Mynum(1, 7, ‘_’, 0);
// Write_Mynum(1, 8, ‘_’, 0);
  for (int i = 1; i <= 8; i++) {
    Write_Mynum(2, i, 2, 0);
  }

  for (int i = 1; i <= 8; i++) {
    Write_Mynum(3, i, 3, 0);
  }

  for (int i = 1; i <= 8; i++) {
    Write_Mynum(4, i, 8 – i, 1);
  }

  while (1);
}


void Delay_xms(unsigned int x)
{
  unsigned int i, j;
  for (i = 0; i < x; i++)
    for (j = 0; j < 112; j++);
}

//切换地址,方便写
void Write_Mynum(int pnum,  unsigned char address, unsigned char dat , int dp) {
  Write_Max7219(pnum, 9 – address, dat, dp);
}

//——————————————–
//功能:向MAX7219写入字节
//入口参数:DATA,dp显示小数点与否
void Write_Max7219_byte(unsigned char DATA, int dp)
{
  unsigned char i;
  digitalWrite(CS, LOW);
  for (i = 8; i >= 1; i–)
  {
    digitalWrite(CLK, LOW);
    if (i == 8 && dp == 1)
      digitalWrite(DIN, HIGH);
    else {
      if (DATA & 0X80)
        digitalWrite(DIN, HIGH);
      else
        digitalWrite(DIN, LOW);
    }
    DATA <<= 1;
    digitalWrite(CLK, HIGH);
  }
}

//——————————————-
//功能:向MAX7219写入数据
//入口参数:pnum数码管片序号,address,dat,dp显示小数点与否
void Write_Max7219(int pnum, unsigned char address, unsigned char dat, int dp)
{
  digitalWrite(CS, LOW);
  Write_Max7219_byte(address, 0);          //写入地址,即数码管编号
  Write_Max7219_byte(dat, dp);              //写入数据,即数码管显示数字
  if (pnum > 1) {
    digitalWrite(CLK, HIGH);
    for (int i = 1; i < pnum; i++) {
      Write_Max7219_byte(0X00, 0);
      Write_Max7219_byte(0X00, 0);
    }
  }
  digitalWrite(CS, HIGH);
}


//Max7219初始化
void Init_MAX7219(int pienum)
{
  for (int i = 1 ; i <= pienum ; i++) {
    Write_Max7219(i, 0x09, 0xff, 0);     //译码方式:BCD码
    Write_Max7219(i, 0x0a, 0x03, 0);     //参数3:亮度
    Write_Max7219(i, 0x0b, 0x07, 0);     //扫描界限;参数3:8个数码管显示
    Write_Max7219(i, 0x0c, 0x01, 0);     //掉电模式:参数3:0,普通模式:1
    Write_Max7219(i, 0x0f, 0x01, 0);     //显示测试:参数3:1;测试结束,正常显示:0
  }
}
复制代码

STM32之FLASH驱动

本文介绍如何使用STM32标准外设库驱动FLASH,本例程驱动的FLASH为W25Q64。

本文适合对单片机及C语言有一定基础的开发人员阅读,MCU使用STM32F103VE系列。

1. FLASH简介

FLASH存储器又称为闪存,为可重复擦写的存储器,容量比EEPROM大的多。

FLASH在写入数据时只能把1改成0,而0无法直接改成1,因此要写入数据时,必须先执行擦除操作,而一次擦除操作无法仅擦除一个字节,必须将一整块区域的数据全部改成1。因此FLASH操作的特性是擦除时必须一次擦除一整块区域;写入时可以按字节或按块写入;读取则不受限制,可以读取一个字节和任意多个字节。

FLASH可分为NOR FLASH和NAND FLASH,两者特性有所区别,NOR FLASH读取速度快、可以按字节读写,但容量相同的情况下价格较高,而NAND FLASH读取速度较慢,只能按块为单位读写,但容量相同的情况下价格较低。一般NOR FLASH适用于存储程序代码,NAND FLASH适用于存储大数据量存储。

2. 常用FLASH

一般常用的NOR FLASH为Winbond公司的W25Qxx系列,常用容量从16M到256Mbit不等,换算成字节为2M到32MBytes,可以根据项目需求和价格综合考虑选型。

3.FLASH操作说明

以W25Q64举例,W25Q64容量为64Mbit,即8MByte,地址范围0~0x800000,3个字节即可表示,因此地址长度为3字节。

W25Q64共分为128个Block,每个Block为64Kbytes,每个Block又分为16个Sector,每个Sector为4Kbytes,每个Sector又可分为16个Pages,每个Page有256个字节,每次擦除时至少需要擦除一整个Sector,写入时则可以单字节写入,也可以写入多个字节,但最多写入一个Page,即256个字节。

3.1. 设备ID

W25Q64厂商号为0xEF,FLASH型号为0x4017,可以读取这些信息判定FLASH是否正常。

3.2. 指令

该表中的第一列为指令名,第二列为指令编码,第三至第 N 列的具体内容根据指令的不同而有不同的含义。其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23”指 FLASH 芯片内部存储器组织的地址;“M0~M7”为厂商号(MANUFACTURER ID);“ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7”为 FLASH 内部存储矩阵的内容。

3.3. 读取

使用读取命令(指令编码为03h),发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回内部存储的数据,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。

3.4. 写使能、写禁用和状态读取

在向 FLASH 写入数据或者擦除前,首先要使能写操作,通过发送“Write Enable”命令使FLASH可写,写入或者擦除完毕之后,FLASH自动进入写禁用状态,无需单独发送写禁用命令,当FLASH为写禁用状态时,任何写入或擦除操作无效,这样可避免误写入或误擦除。

由于FLASH写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以在写操作后需要确认 FLASH 芯片“空闲”时才能进行再次写入。为了表示自己的工作状态,FLASH 芯片定义了一个状态寄存器,这个状态寄存器的第 0 位为“BUSY”,当这个位为“1”时,表明 FLASH芯片处于忙碌状态,它可能正在进行“擦除”或“数据写入”的操作。利用指令表中的“Read Status Register”指令可以获取 FLASH 状态寄存器的内容,只要向 FLASH 芯片发送了读状态寄存器的指令,FLASH 芯片就会持续向主机返回最新的状态寄存器内容,直到收到 SPI通讯的停止信号。因此可以通过查看该位,直到该位为0时,即可对FLASH进行擦除或者写操作。如果刚写完数据就执行读操作,也需要等待。

3.5. 擦除

FLASH写入数据之前需要先擦除,擦除可分为扇区擦除(Sector Erase)、块擦除(Block Erase)和整片擦除(Chip Erase)。指令编码分别为20h、D8h,而整片擦除支持2个命令,即2个命令均可使用,为C7h和60h。要实现擦除操作时先发送指令编码,扇区擦除和块擦除需要继续发送要擦除区域的地址,而整片擦除无需发送地址。要执行擦除操作之前需要确保FLASH处于写使能状态,可通过发送写使能命令实现。

3.6. 写入

使用页写入命令(指令编码为02h),先发送指令编码,然后发送要写的起始地址,然后继续发送要写入的内容,一次写入操作最多写入256字节数据。 进行写入之前需要确保FLASH处于写使能状态,可通过发送写使能命令实现。如果想要一次写入超过256字节,那么就需要对页写入命令进行封装。

 

完整代码(仅自己编写的部分)

复制代码
  1 #include "flash.h" 
2 #include "delay.h"
3 #include <stdio.h>
4
5 #define FLASH_PAGE_SIZE 256 //W25Q64每页256个字节
6
7 #define W25X_WriteEnable 0x06
8 #define W25X_WriteDisable 0x04
9 #define W25X_ReadStatusReg 0x05
10 #define W25X_WriteStatusReg 0x01
11 #define W25X_ReadData 0x03
12 #define W25X_FastReadData 0x0B
13 #define W25X_FastReadDual 0x3B
14 #define W25X_PageProgram 0x02
15 #define W25X_BlockErase 0xD8
16 #define W25X_SectorErase 0x20
17 #define W25X_ChipErase 0xC7
18 #define W25X_PowerDown 0xB9
19 #define W25X_ReleasePowerDown 0xAB
20 #define W25X_DeviceID 0xAB
21 #define W25X_ManufactDeviceID 0x90
22 #define W25X_JedecDeviceID 0x9F
23
24 /* WIP(busy)标志,FLASH内部正在写入 */
25 #define WIP_Flag 0x01
26
27 //初始化FLASH接口
28 void FLASH_Init(void)
29 {
30 SPI_IoInit();
31 }
32
33 //查看W25Q64是否空闲
34 //返回值: 1,FLASH忙,无法读写
35 // 0,FLASH空闲,可以读写
36 //注意执行完此函数后,FLASH已取消选中,如果要写入,必须重新选中
37 uint8_t FLASH_WaitReady(void)
38 {
39 uint32_t i = 0;
40 uint8_t ret = 1;
41 uint8_t status = 0;
42
43 SPI_CS_0;
44
45 SPI_WriteByte(W25X_ReadStatusReg);
46
47 for(i = 0; i < 1000; i++){
48 status = SPI_ReadByte();
49 if((status & WIP_Flag) == RESET){
50 ret = 0;
51 break;
52 }
53 delay_ms(10);
54 }
55
56 SPI_CS_1;
57
58 return ret;
59 }
60
61 /*
62 FLASH擦除、写入数据完毕后会自动禁用写使能,因此无需再执行写禁用操作
63 注意执行此函数前,必须先选中FLASH
64 */
65 void FLASH_WriteEnable(void)
66 {
67 /* 发送写使能命令*/
68 SPI_WriteByte(W25X_WriteEnable);
69 }
70
71 uint32_t FLASH_ReadJedecID(void)
72 {
73 uint32_t temp, temp0, temp1, temp2;
74
75 SPI_CS_0;
76
77 /* 发送JEDEC指令,读取ID */
78 SPI_WriteByte(W25X_JedecDeviceID);
79
80 temp0 = SPI_ReadByte();
81 temp1 = SPI_ReadByte();
82 temp2 = SPI_ReadByte();
83
84 /*把数据组合起来,作为函数的返回值*/
85 temp = (temp0 << 16) | (temp1 << 8) | temp2;
86
87 SPI_CS_1;
88
89 return temp;
90 }
91
92 uint8_t FLASH_SectorErase(uint32_t addr)
93 {
94 /* 判断FLASH是否可写,如果不可写,直接返回错误 */
95 if(FLASH_WaitReady()){
96 return 1;
97 }
98
99 SPI_CS_0;
100
101 /* 发送FLASH写使能命令 */
102 FLASH_WriteEnable();
103
104 /* 发送扇区擦除指令*/
105 SPI_WriteByte(W25X_SectorErase);
106 /*发送擦除扇区地址的高位*/
107 SPI_WriteByte((addr & 0xFF0000) >> 16);
108 /* 发送擦除扇区地址的中位 */
109 SPI_WriteByte((addr & 0xFF00) >> 8);
110 /* 发送擦除扇区地址的低位 */
111 SPI_WriteByte(addr & 0xFF);
112 /* 发送FLASH写禁用命令 */
113 // FLASH_WriteDisable();
114
115 SPI_CS_1;
116
117 if(FLASH_WaitReady()){
118 return 2;
119 }
120
121 return 0;
122 }
123
124 uint8_t FLASH_ChipErase(void)
125 {
126 /* 判断FLASH是否可写,如果不可写,直接返回错误 */
127 if(FLASH_WaitReady()){
128 return 1;
129 }
130
131 SPI_CS_0;
132
133 /* 发送FLASH写使能命令 */
134 FLASH_WriteEnable();
135
136 /* 发送扇区擦除指令*/
137 SPI_WriteByte(W25X_ChipErase);
138
139 SPI_CS_1;
140
141 if(FLASH_WaitReady()){
142 return 2;
143 }
144
145 return 0;
146 }
147
148 //在W25Q64里面的指定地址开始读出指定个数的数据
149 //addr: 开始读数的地址
150 //pBuffer: 需要读取数据的指针
151 //numToRead:要读出数据的个数
152 //返回值: 1,读取失败
153 // 0,读取成功
154 uint8_t FLASH_Read(uint32_t addr, uint8_t *pBuffer, uint32_t numToRead)
155 {
156 SPI_CS_0;
157
158 SPI_WriteByte(W25X_ReadData);
159
160 /* 发送 读 地址高位 */
161 SPI_WriteByte((addr & 0xFF0000) >> 16);
162 /* 发送 读 地址中位 */
163 SPI_WriteByte((addr & 0xFF00) >> 8);
164 /* 发送 读 地址低位 */
165 SPI_WriteByte(addr & 0xFF);
166
167 /* 读取数据 */
168 while (numToRead--) /* while there is data to be read */
169 {
170 /* 读取一个字节*/
171 *pBuffer++ = SPI_ReadByte();
172 }
173
174 SPI_CS_1;
175
176 return 0;
177 }
178
179 //在W25Q64指定地址读出一个数据
180 //addr: 开始读数的地址
181 //pReadData:需要读取数据的指针
182 //返回值: 1,读取失败
183 // 0,读取成功
184 uint8_t FLASH_ByteRead(uint32_t addr, uint8_t * pReadData)
185 {
186 SPI_CS_0;
187
188 SPI_WriteByte(W25X_ReadData);
189
190 /* 发送 读 地址高位 */
191 SPI_WriteByte((addr & 0xFF0000) >> 16);
192 /* 发送 读 地址中位 */
193 SPI_WriteByte((addr & 0xFF00) >> 8);
194 /* 发送 读 地址低位 */
195 SPI_WriteByte(addr & 0xFF);
196
197 /* 读取一个字节*/
198 *pReadData = SPI_ReadByte();
199
200 SPI_CS_1;
201
202 return 0;
203 }
204
205 //在W25Q64指定地址写入一个数据
206 //addr: 写入数据的目的地址
207 //dataToWrite: 要写入的数据
208 //返回值: 1,写入失败
209 // 0,写入成功
210 uint8_t FLASH_ByteWrite(uint32_t addr, uint8_t dataToWrite)
211 {
212 /* 判断FLASH是否可写,如果不可写,直接返回错误 */
213 if(FLASH_WaitReady()){
214 return 1;
215 }
216
217 SPI_CS_0;
218
219 /* 发送FLASH写使能命令 */
220 FLASH_WriteEnable();
221
222 /* 写页写指令*/
223 SPI_WriteByte(W25X_PageProgram);
224 /*发送写地址的高位*/
225 SPI_WriteByte((addr & 0xFF0000) >> 16);
226 /*发送写地址的中位*/
227 SPI_WriteByte((addr & 0xFF00) >> 8);
228 /*发送写地址的低位*/
229 SPI_WriteByte(addr & 0xFF);
230
231 /* 发送当前要写入的字节数据 */
232 SPI_WriteByte(dataToWrite);
233
234 SPI_CS_1;
235
236 if(FLASH_WaitReady()){
237 return 2;
238 }
239
240 return 0;
241 }
242
243 uint8_t FLASH_PageWrite(uint32_t addr, uint8_t *pBuffer, uint32_t numToWrite)
244 {
245 /* 判断FLASH是否可写,如果不可写,直接返回错误 */
246 if(FLASH_WaitReady()){
247 return 1;
248 }
249
250 SPI_CS_0;
251
252 /* 发送FLASH写使能命令 */
253 FLASH_WriteEnable();
254
255 /* 写页写指令*/
256 SPI_WriteByte(W25X_PageProgram);
257 /*发送写地址的高位*/
258 SPI_WriteByte((addr & 0xFF0000) >> 16);
259 /*发送写地址的中位*/
260 SPI_WriteByte((addr & 0xFF00) >> 8);
261 /*发送写地址的低位*/
262 SPI_WriteByte(addr & 0xFF);
263
264 /* 写入数据*/
265 while(numToWrite--)
266 {
267 /* 发送当前要写入的字节数据 */
268 SPI_WriteByte(*pBuffer++);
269 }
270
271 SPI_CS_1;
272
273 if(FLASH_WaitReady()){
274 return 2;
275 }
276
277 return 0;
278 }
279
280 /*
281 根据要写入的地址、长度、页大小计算如何分页
282 输入参数:addr: 写入起始地址
283 len: 写入数据长度
284 pageSize:每页存储的数据,对于W25Q64来说,该值为256
285 要写入参数:pFirstPageLen: 首页要写入的字节
286 pLastPageLen: 尾页要写入的字节
287 pPageNum: 总共要写入的页数
288 */
289 void FLASH_GetWritePages(uint32_t addr, uint32_t len, uint32_t pageSize,
290 uint32_t * pFirstPageLen, uint32_t * pLastPageLen, uint32_t * pPageNum)
291 {
292 uint32_t firstPageOffset; //首页偏移
293 uint32_t otherLen; //去除首页之后剩余长度
294 uint32_t otherPageNum; //去除首页之后剩余整数页数量
295
296 firstPageOffset = addr % pageSize;
297 *pFirstPageLen = pageSize - firstPageOffset;
298
299 if(len < *pFirstPageLen){
300 *pFirstPageLen = len;
301 }
302
303 otherLen = len - *pFirstPageLen;
304 otherPageNum = otherLen / pageSize;
305 *pLastPageLen = otherLen % pageSize;
306
307 *pPageNum = otherPageNum + 1;
308
309 if(*pLastPageLen){
310 (*pPageNum)++;
311 }
312 }
313
314
315 //在W25Q64里面的指定地址开始写入指定个数的数据
316 //addr: 开始读数的地址
317 //pBuffer: 需要读取数据的指针
318 //NumToWrite:要写入数据的个数
319 //返回值: 1,读取失败
320 // 0,读取成功
321 uint8_t FLASH_Write(uint32_t addr, uint8_t *pBuffer, uint32_t numToWrite)
322 {
323 uint32_t i;
324 uint32_t firstPageLen, lastPageLen, pageNum;
325
326 FLASH_GetWritePages(addr, numToWrite, FLASH_PAGE_SIZE,
327 &firstPageLen, &lastPageLen, &pageNum);
328
329 printf("addr:%#x, numToWrite:%d, firstPageLen:%d, lastPageLen:%d, pageNum:%d\n",
330 addr, numToWrite, firstPageLen, lastPageLen, pageNum);
331
332 for(i = 0; i < pageNum; i++)
333 {
334 if(i == 0){ //首页写入长度为firstPageLen
335 if(FLASH_PageWrite(addr, pBuffer, firstPageLen)){
336 goto write_fail;
337 }
338 addr += firstPageLen;
339 pBuffer += firstPageLen;
340 }else if(i == pageNum - 1){ //尾页写入长度为lastPageLen
341 if(FLASH_PageWrite(addr, pBuffer, lastPageLen)){
342 goto write_fail;
343 }
344 addr += lastPageLen;
345 pBuffer += lastPageLen;
346 }else{ //除首页和尾页外写入长度为FLASH_PAGE_SIZE
347 if(FLASH_PageWrite(addr, pBuffer, FLASH_PAGE_SIZE)){
348 goto write_fail;
349 }
350 addr += FLASH_PAGE_SIZE;
351 pBuffer += FLASH_PAGE_SIZE;
352 }
353 }
354
355 return 0;
356
357 write_fail:
358 return 1;
359 }

本文转载自“https://www.cnblogs.com/greatpumpkin/p/13747892.html”