Linux-SPI-位修改指令-屏蔽字节-修改CANINE寄存器的值-修改工作模式

提示:Linux-SPI-位修改指令-屏蔽字节-修改CANINE寄存器的值-修改工作模式

文章目录


前言

在之前笔记中,特别在 Linux驱动-SPI-MCP2515外设模块写CNF1-CNF2-CNF3-RXB0CTRL寄存器值 文章中, 我们对寄存器进行了写入操作。 CNF1/CNF2/CNF3 寄存器值在MCP2515 进行复位以后,首先要对几个寄存器进行写值操作,其实就是设置波特率操作。

  • 除了要配置 CNF1、 CNF2、 CNF3 三个 CAN 波特率配置寄存器之外, 还需要对 RXB0CTRL 接收缓冲器 0 控制寄存器进行配置;
  • 最后还需要对 CANINTE------中断使能寄存器进行配置

其实就是复位后还要做3件事:设置Can波特率;设置CAN收发配置;设置中断 这些基本操作。

一、参考资料

Linux驱动-SPI-MCP2515外设模块写CNF1-CNF2-CNF3-RXB0CTRL寄存器值

RK3568驱动指南|第十六篇 SPI-第190章 配置模式下寄存器的配置

RK3568驱动指南|第十六篇 SPI-第191章 mcp2515驱动编写:修改工作模式

二、概念理解

基本概念:

  • 位修改指令(Modify Bit instruction) 是一种指令或操作, 允许用户在处理器或芯片的寄存器中修改单个位(bit) , 而不影响寄存器中的其他位。
  • 屏蔽字节(Mask Byte) : 屏蔽处理器或芯片寄存器中特定位的字节值。 屏蔽字节决定了在执行位修改指令时哪些位会被修改, 哪些位会保持不变。 通过在位修改指令中使用屏蔽字节,可以精确地指定要修改的寄存器位, 而不会影响其他位的内容。

也就是说之前进行的是对字节 8个位一起操作,现在对其中一个位进行修改操作。

基本规则

  • 屏蔽字节确定允许修改寄存器中的哪一位。 屏蔽字节中的"1" 表示允许对寄存器中的相应位进行修改; 而"0" 则禁止修改。 数据字节确定寄存器位修改的最终结果。 如果屏蔽字节 相应位置为"1" , 数据字节中的"1" 表示将寄存器对 应位设置为 1, 而"0" 将对该位清零

其实很好理解的: 对于位的修改,你要先告诉我修改哪一位或者哪几位,然后修改值是怎样的。 对于非屏蔽位修改无用的。 结合上面的图自行理解下,我举个简单例子吧吧:

一个字节:

三、实验

1、查看数据手册-验证寄存器是否可以修改

位指令修改并不是所有的IC都支持的,所以自己需要根据使用手册、数据手册查一下支不支持这个功能且并不是所有的寄存器都支持位修改,这个也要看一下数据手册的。

说明 外设IC是支持位修改指令的。

2、位指令修改CANINTE寄存器值

a、查看寄存器地址

寄存器地址:ox60

b、查看寄存器组成-是否支持位修改

寄存器组成-2、5、6三个位是支持位修改的。 那么在设置对应功能时候直接用位修改的api 进行操作即可。

c、设置计算-计算需要配置寄存器的值-位修改的值

  • 计算需要配置寄存器的值:需要将 bit6-5 设置为 11, 表示接收所有的报文, 其他位都设置为 0 即可, 所以该寄存器要设置的二进制值为 01100000, 换算成 16 进制就为 0x60
  • 计算需要屏蔽的值:2、5、6 位进行修改不就是:01100100 , 得到十六进制:0x64

d、查看位修改寄存器报文格式

从位修改指令的报文格式来看,需要的一定有四个数据:位修改指令、地址字节、屏蔽字节、数据字节

e、实现位修改需求

如上:从位修改指令的报文格式来看,需要的一定有四个数据:位修改指令、地址字节、屏蔽字节、数据字节4个数据。

4个数据值分别如下:

  • 位修改指令:ox05
  • 地址字节: ox60
  • 屏蔽字节:0x64
  • 数据字节:0x60

假如直接通过SPI 写数据,给寄存器写值:

java 复制代码
#define RXB0CTRL 0x60

void mcp2515_write_reg(char reg, char value) {
int ret;
char write_buf[] = {0x02, reg, value}; // SPI 写缓冲区, 用于发送写寄存器命令
ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送 SPI 写命令
if (ret < 0) {
printk("mcp2515_write_reg error\n");
}
}

mcp2515_write_reg(RXB0CTRL , 0x60);

那么通过位修改方案来对寄存器写值,如何实现? 如下:

java 复制代码
#define RXB0CTRL 0x60

// MCP2515 修改寄存器位函数
void mcp2515_change_regbit(char reg, char mask, char value) {
int ret;
char write_buf[] = { 0x05, reg, mask, value }; // SPI 写缓冲区, 用于发送修改寄存器位命令
ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送 SPI 写命令
if (ret < 0) {
printk("mcp2515_change_regbit error\n");
}
}


mcp2515_change_regbit(RXB0CTRL, 0x64, 0x60);

3、位指令修改工作模式

修改工作模式 方法和修改CANINTE寄存器值 需求是一摸一样的:

  • 确认位修改指令
  • 确认工作模式寄存器地址值
  • 需要修改为工作模式的值
  • 修改模式需要设置的屏蔽字节

    CANCTRL 寄存器地址为 0X0f, bit7-bit5 用来配置工作模式, 环会模式为 010, 而为了改变其他位, 可以使用位修改函数对该寄存器进行修改, 具体内容如下所示
java 复制代码
#define CANCTRL 0x0f // CAN 状态寄存器
mcp2515_change_regbit(CANCTRL, 0xe0, 0x40);

0xe0 为掩码, 换算成二进制之后为 11100000, 表示只对高三位进行修改, 修改的值为 010,表示要设置为回环模式

四、驱动程序-验证

参考 Linux驱动-SPI-读寄存器函数

java 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>

#define CNF1 0x2a                    // 寄存器定义
#define CNF2 0x29
#define CNF3 0x28
#define RXB0CTRL 0x60
#define CANINTE 0x2b
#define CANCTRL 0xf                    // CAN控制寄存器

#define TXB0CTRL 0x30   //发送缓冲器控制寄存器地址
#define CANINTF 0x2c 

dev_t dev_num; // 设备号
struct cdev mcp2515_cdev; // 字符设备结构体
struct class *mcp2515_class; // 设备类
struct device *mcp2515_device; // 设备
struct spi_device *spi_dev; // SPI设备指针


// MCP2515芯片复位函数
void mcp2515_reset(void){
    int ret;
    char write_buf[] = {0xc0}; // 复位指令0x11000000即0xc0
    ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送复位命令
    if(ret < 0){
        printk("spi_write is error\n"); // 打印错误信息
    }
}

// MCP2515读寄存器函数
char mcp2515_read_reg(char reg) {
    char write_buf[] = {0x03, reg};  // SPI写缓冲区写入SPI读指令0x03
    char read_buf;                   // SPI读缓冲区
    int ret;

    ret = spi_write_then_read(spi_dev, write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf));  // 调用SPI写读函数
    if (ret < 0) {
        printk("spi_write_then_read error\n");
        return ret;
    }

    return read_buf;
}

//  MCP2515写寄存器函数
void mcp2515_write_reg(char reg, char value) {
    int ret;
    char write_buf[] = {0x02, reg, value};  // SPI写缓冲区,用于发送写寄存器命令

    ret = spi_write(spi_dev, write_buf, sizeof(write_buf));  // 发送SPI写命令
    if (ret < 0) {
        printk("mcp2515_write_reg error\n");
    }
}

// MCP2515修改寄存器位函数
void mcp2515_change_regbit(char reg, char mask, char value) {
    int ret;
    char write_buf[] = { 0x05, reg, mask, value };  // SPI写缓冲区,用于发送修改寄存器位命令

    ret = spi_write(spi_dev, write_buf, sizeof(write_buf));  // 发送SPI写命令
    if (ret < 0) {
        printk("mcp2515_change_regbit error\n");
    }
}

// 打开设备文件的回调函数
int mcp2515_open(struct inode *inode, struct file *file) {
    return 0; // 返回成功
}


// 读设备操作函数,从设备读取数据到用户缓冲区
ssize_t mcp2515_read(struct file *file, char __user *buf, size_t size, loff_t *offset) {
    char r_kbuf[13] = {0};  // 内核缓冲区,用于存储从设备读取的数据
    int i;
    int ret;

    // 等待接收缓冲区满标志位被设置
    while (!(mcp2515_read_reg(CANINTF) & (1 << 0)));

    // 从接收缓冲区读取数据到内核缓冲区
    for (i = 0; i < sizeof(r_kbuf); i++) {
        r_kbuf[i] = mcp2515_read_reg(0x61 + i);
    }

    // 清除接收缓冲区满标志位
    mcp2515_change_regbit(CANINTF, 0x01, 0x00);

    // 将内核缓冲区的数据复制到用户缓冲区
    ret = copy_to_user(buf, r_kbuf, size);
    if (ret) {
        printk("copy_to_user r_kbuf is error\n");
        return -1;  // 返回-1表示复制数据失败
    }

    return 0;  // 返回0表示成功读取数据
}

// 写设备操作函数
ssize_t mcp2515_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) {
    char w_kbuf[13] = {0};
    int ret;
    int i;

    // 设置TXB0CTRL寄存器的部分位
    mcp2515_change_regbit(TXB0CTRL, 0x03, 0x03);

    // 从用户空间复制数据到内核缓冲区
    ret = copy_from_user(w_kbuf, buf, size);
    if (ret) {
        printk("copy_from_user w_kbuf is error\n");
        return -1;
    }

    // 将数据写入MCP2515寄存器
    for (i = 0; i < sizeof(w_kbuf); i++) {
        mcp2515_write_reg(0x31 + i, w_kbuf[i]);
    }

    // 设置TXB0CTRL寄存器的部分位,启动发送
    mcp2515_change_regbit(TXB0CTRL, 0x08, 0x08);
    
    // 等待发送完成
    while (!(mcp2515_read_reg(CANINTF) & (1 << 2)));

    // 清除发送完成标志
    mcp2515_change_regbit(CANINTF, 0x04, 0x00);

    return size;
}

// 关闭设备文件的回调函数
int mcp2515_release(struct inode *inode, struct file *file) {
    return 0; // 返回成功
}

// 设备文件操作集合
struct file_operations mcp2515_fops = {
    .open = mcp2515_open,
    .read = mcp2515_read,
    .write = mcp2515_write,
    .release = mcp2515_release,
};

// MCP2515设备初始化函数
int mcp2515_probe(struct spi_device *spi) {
    int ret, value;
    printk("This is mcp2515_probe\n");

spi_dev = spi; // 保存SPI设备指针
    // 分配字符设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, "mcp2515");
    if (ret < 0) {
        printk("alloc_chrdev_region error\n");
    }

    // 初始化字符设备
    cdev_init(&mcp2515_cdev, &mcp2515_fops);
    mcp2515_cdev.owner = THIS_MODULE;

    // 添加字符设备
    ret = cdev_add(&mcp2515_cdev, dev_num, 1);
    if (ret < 0) {
        printk("cdev_add error\n");
        return -1;
    }

    // 创建设备类
    mcp2515_class = class_create(THIS_MODULE, "spi_to_can");
    if (IS_ERR(mcp2515_class)) {
        printk("mcp2515_class error\n");
        return PTR_ERR(mcp2515_class);
    }

    // 创建设备
    mcp2515_device = device_create(mcp2515_class, NULL, dev_num, NULL, "mcp2515");
    if (IS_ERR(mcp2515_device)) {
        printk("mcp2515_device error\n");
        return PTR_ERR(mcp2515_device);
    }

    mcp2515_reset();             // 复位MCP2515设备
    value = mcp2515_read_reg(0x0e);  // 读取寄存器值
    printk("value is %x\n", value);  // 打印读取的值
	
	mcp2515_write_reg(CNF1, 0x01);  // 写入寄存器配置值
	mcp2515_write_reg(CNF2, 0xb1);
	mcp2515_write_reg(CNF3, 0x05);

	mcp2515_write_reg(RXB0CTRL, 0x60);
	mcp2515_write_reg(CANINTE, 0x05);
	mcp2515_change_regbit(CANCTRL, 0xe0, 0x40);
	
	value = mcp2515_read_reg(0x0e);  // 读取寄存器值
    printk("value is %x\n", value);  // 打印读取的值
	
    return 0; // 返回成功
}

// MCP2515 SPI设备的移除函数
static int mcp2515_remove(struct spi_device *spi) {
    device_destroy(mcp2515_class, dev_num);
    class_destroy(mcp2515_class);
    cdev_del(&mcp2515_cdev);
    unregister_chrdev_region(dev_num, 1);
    return 0;
}

// MCP2515设备匹配表,用于设备树匹配
static const struct of_device_id mcp2515_of_match_table[] = {
    { .compatible = "my-mcp2515" },
    {}
};

// MCP2515设备ID匹配表,用于总线匹配
static const struct spi_device_id mcp2515_id_table[] = {
    { "mcp2515", 0 },
    {}
};

// MCP2515 SPI驱动结构体
static struct spi_driver spi_mcp2515 = {
    .probe = mcp2515_probe, // 探测函数
    .remove = mcp2515_remove, // 移除函数
    .driver = {
        .name = "mcp2515", // 驱动名称
        .owner = THIS_MODULE, // 所属模块
        .of_match_table = mcp2515_of_match_table, // 设备树匹配表
},
    .id_table = mcp2515_id_table, // 设备ID匹配表
};

// 驱动初始化函数
static int __init mcp2515_init(void)
{
    int ret;

    // 注册SPI驱动
    ret = spi_register_driver(&spi_mcp2515);
    if (ret < 0) {
        // 注册失败,打印错误信息
        printk("spi_register_driver error\n");
        return ret;
    }

    return ret;
}

// 驱动退出函数
static void __exit mcp2515_exit(void)
{
    // 注销SPI驱动
    spi_unregister_driver(&spi_mcp2515);
}

module_init(mcp2515_init);
module_exit(mcp2515_exit);

MODULE_LICENSE("GPL");

核心代码:

最终结果如下:

java 复制代码
[root@topeet:/mnt/sdcard]# [ 1204.602120] This is mcp2515_probe
[ 1204.602930] [MCP2515] READ REG[0x0E] = 0x80
[ 1204.602959] value is 80
[ 1204.603274] [MCP2515] READ REG[0x0E] = 0x40
[ 1204.603293] value is 40

五、位修改方案存在的价值意义

位修改指令 存在的意义是什么。 本身对 寄存器操作 直接对地址操作 写寄存器地址和寄存器值就可以了。 位修改操作只修改对应的位上的值,存在的意义是什么。

从"最终结果"看,写整个寄存器和写单个位,确实都能让那一位变成1或0 但"位修改指令"(如ARM的STRB/BFI,51单片机的SETB/CLR,或者X86的BTS)存在的意义,从来不在于"改那一位",而在于"不改其他位"时的安全性和效率。

它解决的不是"怎么写值"的问题,而是"怎么读-改-写"这个原子过程的问题。

1. 解决"读-改-写"的竞态风险(最关键)

这是位操作指令存在的核心生死线。

假设没有位修改指令,你要改寄存器(或内存)的第3位,必须做三步:

  • 读:读取整个32位寄存器的值。
  • 改:在CPU内部用 与/或 运算修改第3位。
  • 写:把整个32位值写回寄存器。

**致命问题:**如果这3步之间,发生了中断(ISR)或RTOS任务切换,而ISR里也修改了这个寄存器的其他位,那么ISR执行完后,回到主程序,主程序会把旧的32位值(ISR修改前的值)写回去------ISR的修改被覆盖了,这叫"丢失的修改"。

而硬件级的位修改指令(如SETB或BTS),CPU在硬件层面保证读、修改、写在一个总线周期内完成,不可被打断。这不是方便,这是多任务环境下寄存器配置不出错的保命手段。

2. 节省代码体积和总线带宽(效率问题)

  • 代码体积:如果不用位指令,你需要加载立即数(如0x00000008)、做或运算、再写回,至少3条指令。而位修改指令(如BSET R0, #3)只有一条指令。在Flash昂贵的嵌入式芯片里,这很值钱。

  • 总线带宽:很多MCU(如Cortex-M)的位带(Bit-Band)特性,把1位映射到1个32位的别名地址。你写这个别名地址,总线硬件自动帮你完成读-改-写。虽然最终总线还是读了整个字,但CPU只发了一次写操作,节省了CPU流水线停顿。

核心区别

直接写整个寄存器,是在"你知道所有位该是什么值"时用的(如初始化)。

位修改指令,是在"你只知道这一位该是什么值,其他位不能动"时用的,且它能保证这个过程在硬件上原子完成,不被中断撕碎。

总结

  • 位修改指令一定要搞明白原理
  • 实际使用中有用到的,搞清楚原理会用
  • 这几章节学习 始终一个核心技能:看使用手册然后根据使用手册进行程序编写