笔记目录
四. I2C从设备驱动编程
1. 重要结构体
struct i2c_driver {
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
struct device_driver driver;
const struct i2c_device_id *id_table;
};
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
2. 创建i2c_client
1> 使用设备树方式
参考:Documentation/devicetree/bindings/i2c/i2c-stm32.txt
修改linux-5.4.31/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts文件
&i2c1 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&i2c1_pins_b>;
pinctrl-1 = <&i2c1_pins_sleep_b>;
i2c-scl-rising-time-ns = <100>;
i2c-scl-falling-time-ns = <7>;
status = "okay";
/delete-property/dmas;
/delete-property/dma-names;
si7006@40{
compatible = "stm32mp157,i2c_si7006";
reg = <0x40>;
};
};
//初始化引脚
修改linux-5.4.31/arch/arm/boot/dts/stm32mp15-pinctrl.dtsi
i2c1_pins_sleep_b: i2c1-1 {
pins {
pinmux = <STM32_PINMUX('F', 14, ANALOG)>, /* I2C1_SCL */
<STM32_PINMUX('F', 15, ANALOG)>; /* I2C1_SDA */
};
};
2> 使用代码方式
static struct i2c_client *si7006_client;
static const unsigned short addr_list[] = { 0x40, 0x50, I2C_CLIENT_END};
static int i2c_si7006_dev_init(void)
{
struct i2c_adapter *i2c_adap;
struct i2c_board_info si7006_info;
memset(&si7006_info, 0, sizeof(struct i2c_board_info));
strlcpy(si7006_info.type, "i2c_si7006", I2C_NAME_SIZE);
i2c_adap = i2c_get_adapter(0);
si7006_client = i2c_new_probed_device(i2c_adap, &si7006_info, addr_list, NULL);
i2c_put_adapter(i2c_adap);
if (si7006_client)
return 0;
else
return -ENODEV;
}
static void i2c_si7006_dev_exit(void)
{
i2c_unregister_device(si7006_client);
}
* i2c_new_device
* i2c_new_probed_device(用这个方法前要先使能引脚)
* i2c_register_board_info
* 内核没有`EXPORT_SYMBOL(i2c_register_board_info)`
* 使用这个函数的驱动必须编进内核里去
3> 使用命令行方式
echo i2c_si7006 0x40 > /sys/bus/i2c/devices/i2c-0/new_device
echo 0x40 > /sys/bus/i2c/devices/i2c-0/delete_device
3.温湿度传感器驱动编程思路
-
注册I2C设备
static int __init i2c_si7006_drv_init(void) {//注册I2C设备 return i2c_register_driver(THIS_MODULE, &i2c_si7006_drv); }
-
创建重要结构体
//创建i2c从机结构体指针(传感器) struct i2c_client *si7006_client; //文件操作 const struct file_operations si7006_fops = { .owner = THIS_MODULE, .unlocked_ioctl = i2c_si7006_drv_ioctl, //应用层用到switch调用drv_ioctl驱动接口 }; //杂项驱动 struct miscdevice misc = { .minor = 199, .name = "i2c_si7006", .fops = &si7006_fops, }; //创建I2C总线 const struct of_device_id i2c_si7006_table[] = { { .compatible = "stm32mp157,i2c_si7006"}, { }, }; //I2C驱动结构体 struct i2c_driver i2c_si7006_drv = { .probe = i2c_si7006_drv_probe, .remove = i2c_si7006_drv_remove, .driver = { .name = "stm32_i2c_si7006", .of_match_table = i2c_si7006_table, }, };
-
probe函数
int i2c_si7006_drv_probe(struct i2c_client *client, const struct i2c_device_id *id_table) {//注册杂项设备以及匹配从机 int ret; printk("----------------%s-----------------\n",__FUNCTION__); si7006_client = client; ret = misc_register(&misc); return 0; }
-
remove函数(反操作)
int i2c_si7006_drv_remove(struct i2c_client *client) { printk("----------------%s-----------------\n",__FUNCTION__); misc_deregister(&misc); return 0; }
-
读取温度
//入口参数@addr:从机地址,@reg:从机寄存器地址,@buffer:保存读取数据,@length:reg/buffer的长度 int read_temperature(struct i2c_client *client) { int ret; unsigned char rx_buf[2]; unsigned char tx_buf[] = {0xE3}; struct i2c_msg msg[2];//一个i2c_msg结构体代表着一次单方向的传输 int temp_code; msg[0].addr = client->addr; msg[0].flags = 0;(写信号) msg[0].buf = tx_buf; msg[0].len = sizeof(tx_buf)/sizeof(tx_buf[0]); msg[1].addr = client->addr; msg[1].flags = 1;(读信号) msg[1].buf = rx_buf; msg[1].len = sizeof(rx_buf)/sizeof(rx_buf[0]);、 //i2c_transfer执行上面I2C结构体的操作 ret = i2c_transfer(client->adapter, msg, sizeof(msg)/sizeof(msg[0])); if(ret < 0) { printk("Fail to i2c transfer\n"); return ret; } temp_code = rx_buf[0] << 8 | rx_buf[1];//rx_buf[0]先收到高位数据(例:十位),rx_buf[1]后收到低位(个位) //temperature = (175.72 * temp_code)/ 65536 - 46.85; return temp_code }
-
读取相对湿度
int read_humidity(struct i2c_client *client) { int ret; unsigned char rx_buf[2]; unsigned char tx_buf[] = {0xE5}; struct i2c_msg msg[2]; int hum_code; msg[0].addr = client->addr; msg[0].flags = 0; msg[0].buf = tx_buf; msg[0].len = sizeof(tx_buf)/sizeof(tx_buf[0]); msg[1].addr = client->addr; msg[1].flags = 1; msg[1].buf = rx_buf; msg[1].len = sizeof(rx_buf)/sizeof(rx_buf[0]); ret = i2c_transfer(client->adapter, msg, sizeof(msg)/sizeof(msg[0])); if(ret < 0) { printk("Fail to i2c transfer\n"); return ret; } hum_code = rx_buf[0] << 8 | rx_buf[1]; //humidity = (125 * hum_code)/ 65536 - 6; return hum_code; }
-
ioctl驱动接口
long i2c_si7006_drv_ioctl(struct file * filp, unsigned int cmd, unsigned long arg) { int ret; unsigned int temp; unsigned int hum; switch(cmd) { case SI7006_GET_TEMP: temp = read_temperature(si7006_client); ret = copy_to_user((void __user *)arg, &temp, sizeof(temp)); if(ret > 0) { printk("Fail to copy to user\n"); return -EFAULT; } break; case SI7006_GET_HUM: hum = read_humidity(si7006_client); ret = copy_to_user((void __user *)arg, &hum, sizeof(hum)); if(ret > 0) { printk("Fail to copy to user\n"); return -EFAULT; } break; default: printk("unKnown cmd!\n"); break; } return 0; }
4.温湿度传感器应用层
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/ioctl.h>
//定义获得温度以及相对湿度的特定宏
#define SI7006_GET_TEMP _IOR('S',0x00,short)
#define SI7006_GET_HUM _IOR('S',0x01,short)
int main(void)
{
int fd;
int ret;
float temperature;
int value;
int humidity;
//打开设备节点
fd = open("/dev/i2c_si7006", O_RDWR);
if(fd < 0)
{
perror("open");
exit(1);
}
while(1)
{
//调用驱动层接口获取i2c温度储存到value里
ret = ioctl(fd,SI7006_GET_TEMP,(unsigned long)&value);
if(ret < 0)
{
perror("Fail to ioctl");
return -1;
}
//数学转换
temperature = (175.72 * (unsigned short)value)/ 65536 - 46.85;
printf("Temperature:%.2f\n",temperature);
sleep(1);
//调用驱动层接口获取i2c相对湿度储存到value里
ret = ioctl(fd,SI7006_GET_HUM,(unsigned long)&value);
if(ret < 0)
{
perror("Fail to ioctl");
return -1;
}
//数学转换
humidity = (125 * (unsigned short)value)/ 65536 - 6;
printf("Humidity:%d%%\n",humidity);
sleep(1);
}
close(fd);
}
五. I2C-T>ools使用
Linux内核里面有一个通用的i2c驱动程序—i2c-dev.c,可以下载一个开源的i2c应用—i2c-tools-4.2.tar.xz
i2c-tools-4.2.tar.xz交叉编译完毕后会有很多命令<可执行程序>,运行这些命令<可执行程序>,会调用i2c通用驱动程序i2c-dev.c,实现对从设备的操作!
因此i2c从设备的操作,可以通过命令行来完成,不用自己写驱动程序和应用程序也可以!
1. 交叉编译i2c-tools
farsight@ubuntu:/opt/stm32_sdk$ vi environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
export ARM_CC="arm-ostl-linux-gnueabi-gcc -mthumb -mfpu=neon-vfpv4 -mfloat-abi=hard -mcpu=cortex-a7 --sysroot=$SDKTARGETSYSROOT"
export ARM_AR=arm-ostl-linux-gnueabi-ar
export ARM_STRIP=arm-ostl-linux-gnueabi-strip
修改i2c-tools-4.2/Makefile
CC = $(ARM_CC)
AR = $(ARM_AR)
STRIP = $(ARM_STRIP)
执行make即可
* 执行make时,是动态链接,需要把libi2c.so也放到板子上
* 想静态链接的话,执行:make USE_STATIC_LIB=1
把i2c-tools-4.2/tools中生成的命令拷贝到开发板即可!
2. 使用命令
<1> i2cdetect — I2C检测
1> 列出当前的I2C Adapter(或称为I2C Bus、I2C Controller)
# i2cdetect -l
i2c-1 i2c STM32F7 I2C(0x40013000) I2C adapter
i2c-2 i2c STM32F7 I2C(0x5c002000) I2C adapter
i2c-0 i2c STM32F7 I2C(0x40012000) I2C adapter
2> 打印某个I2C Adapter的Functionalities, I2CBUS为0、1、2等整数
# i2cdetect -F 0
Functionalities implemented by /dev/i2c-0:
I2C yes
SMBus Quick Command yes
SMBus Send Byte yes
SMBus Receive Byte yes
SMBus Write Byte yes
SMBus Read Byte yes
SMBus Write Word yes
SMBus Read Word yes
SMBus Process Call yes
SMBus Block Write yes
SMBus Block Read yes
SMBus Block Process Call yes
SMBus PEC yes
I2C Block Write yes
I2C Block Read yes
3> 看看有哪些I2C设备, I2CBUS为0、1、2等整数
// --表示没有该地址对应的设备, UU表示有该设备并且它已经有驱动程序,
// 数值表示有该设备但是没有对应的设备驱动
# i2cdetect -y -a 0
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 1e --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: UU -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- 57 -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
<2> i2cget — I2C读
# i2cget
Usage: i2cget [-f] [-y] [-a] I2CBUS CHIP-ADDRESS [DATA-ADDRESS [MODE]]
I2CBUS is an integer or an I2C bus name
ADDRESS is an integer (0x03 - 0x77, or 0x00 - 0x7f if -a is given)
MODE is one of:
b (read byte data, default)
w (read word data)
c (write byte/read byte)
Append p for SMBus PEC
1> 读一个字节
I2CBUS为0、1、2等整数, 表示I2C Bus; CHIP-ADDRESS表示设备地址
i2cget -f -y I2CBUS CHIP-ADDRESS
// 读某个地址上的一个字节:
// I2CBUS为0、1、2等整数, 表示I2C Bus
// CHIP-ADDRESS表示设备地址
// DATA-ADDRESS: 芯片上寄存器地址
// MODE:有2个取值, b-使用`SMBus Read Byte`先发出DATA-ADDRESS, 再读一个字节, 中间无P信号
// c-先write byte, 在read byte,中间有P信号
i2cget -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS MODE
2> 读某个地址上的2个字节
// I2CBUS为0、1、2等整数, 表示I2C Bus
// CHIP-ADDRESS表示设备地址
// DATA-ADDRESS: 芯片上寄存器地址
// MODE:w-表示先发出DATA-ADDRESS,再读2个字节
i2cget -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS MODE
<3> i2cset — I2C写
# i2cset
Usage: i2cset [-f] [-y] [-m MASK] [-r] [-a] I2CBUS CHIP-ADDRESS DATA-ADDRESS [VALUE] ... [MODE]
I2CBUS is an integer or an I2C bus name
ADDRESS is an integer (0x03 - 0x77, or 0x00 - 0x7f if -a is given)
MODE is one of:
c (byte, no value)
b (byte data, default)
w (word data)
i (I2C block data)
s (SMBus block data)
Append p for SMBus PEC
// 写一个字节: I2CBUS为0、1、2等整数, 表示I2C Bus; CHIP-ADDRESS表示设备地址
// DATA-ADDRESS就是要写的数据
i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS
// 给address写1个字节(address, value):
// I2CBUS为0、1、2等整数, 表示I2C Bus; CHIP-ADDRESS表示设备地址
// DATA-ADDRESS: 8位芯片寄存器地址;
// VALUE: 8位数值
// MODE: 可以省略,也可以写为b
i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS VALUE [b]
// 给address写2个字节(address, value):
// I2CBUS为0、1、2等整数, 表示I2C Bus; CHIP-ADDRESS表示设备地址
// DATA-ADDRESS: 8位芯片寄存器地址;
// VALUE: 16位数值
// MODE: w
i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS VALUE w
// SMBus Block Write:给address写N个字节的数据
// 发送的数据有:address, N, value1, value2, ..., valueN
// 跟`I2C Block Write`相比, 需要发送长度N
// I2CBUS为0、1、2等整数, 表示I2C Bus; CHIP-ADDRESS表示设备地址
// DATA-ADDRESS: 8位芯片寄存器地址;
// VALUE1~N: N个8位数值
// MODE: s
i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS VALUE1 ... VALUEN s
// I2C Block Write:给address写N个字节的数据
// 发送的数据有:address, value1, value2, ..., valueN
// 跟`SMBus Block Write`相比, 不需要发送长度N
// I2CBUS为0、1、2等整数, 表示I2C Bus; CHIP-ADDRESS表示设备地址
// DATA-ADDRESS: 8位芯片寄存器地址;
// VALUE1~N: N个8位数值
// MODE: i
i2cset -f -y I2CBUS CHIP-ADDRESS DATA-ADDRESS VALUE1 ... VALUEN i
<4> i2ctransfer — I2C传输(不是基于SMBus)
# i2ctransfer
Usage: i2ctransfer [-f] [-y] [-v] [-V] [-a] I2CBUS DESC [DATA] [DESC [DATA]]...
I2CBUS is an integer or an I2C bus name
DESC describes the transfer in the form: {r|w}LENGTH[@address]
1) read/write-flag 2) LENGTH (range 0-65535) 3) I2C address (use last one if omitted)
DATA are LENGTH bytes for a write message. They can be shortened by a suffix:
= (keep value constant until LENGTH)
+ (increase value by 1 until LENGTH)
- (decrease value by 1 until LENGTH)
p (use pseudo random generator until LENGTH with value as seed)
Example (bus 0, read 8 byte at offset 0x64 from EEPROM at 0x50):
# i2ctransfer 0 w1@0x50 0x64 r8
Example (same EEPROM, at offset 0x42 write 0xff 0xfe ... 0xf0):
# i2ctransfer 0 w17@0x50 0x42 0xff-
// Example (bus 0, read 8 byte at offset 0x64 from EEPROM at 0x50):
# i2ctransfer -f -y 0 w1@0x50 0x64 r8
// Example (bus 0, write 3 byte at offset 0x64 from EEPROM at 0x50):
# i2ctransfer -f -y 0 w9@0x50 0x64 val1 val2 val3
// Example
// first: (bus 0, write 3 byte at offset 0x64 from EEPROM at 0x50)
// and then: (bus 0, read 3 byte at offset 0x64 from EEPROM at 0x50)
# i2ctransfer -f -y 0 w9@0x50 0x64 val1 val2 val3 r3@0x50
# i2ctransfer -f -y 0 w9@0x50 0x64 val1 val2 val3 r3 //如果设备地址不变,后面的设备地址可省略
注意:
I2C-Tools可以通过SMBus来访问I2C设备,也可以使用一般的I2C协议来访问I2C设备。
使用一句话概括I2C传输:APP通过I2C Controller与I2C Device传输数据。
在APP里,有这几个问题:
* 怎么指定I2C控制器?
* i2c-dev.c提供为每个I2C控制器(I2C Bus、I2C Adapter)都生成一个设备节点:/dev/i2c-0、/dev/i2c-1等待
* open某个/dev/i2c-X节点,就是去访问该I2C控制器下的设备
* 怎么指定I2C设备?
* 通过ioctl指定I2C设备的地址
* ioctl(file, I2C_SLAVE, address)
* 如果该设备已经有了对应的设备驱动程序,则返回失败
* ioctl(file, I2C_SLAVE_FORCE, address)
* 如果该设备已经有了对应的设备驱动程序
* 但是还是想通过i2c-dev驱动来访问它
* 则使用这个ioctl来指定I2C设备地址
* 怎么传输数据?
* 两种方式
* 一般的I2C方式:ioctl(file, I2C_RDWR, &rdwr)
* SMBus方式:ioctl(file, I2C_SMBUS, &args)
六. 编写I2C_Adapter驱动
1. 设备树
在设备树里构造I2C Bus节点:
i2c-bus-virtual {
compatible = "farsight,i2c-bus-virtual";
};
2. platform_driver
分配、设置、注册platform_driver结构体。
核心是probe函数,它要做这几件事:
- 根据设备树信息设置硬件(引脚、时钟等)
- 分配、设置、注册i2c_apdater
3. i2c_apdater
i2c_apdater核心是master_xfer函数,它的实现取决于硬件,大概代码如下:
static int xxx_master_xfer(struct i2c_adapter *adapter,struct i2c_msg *msgs, int num)
{
for (i = 0; i < num; i++) {
struct i2c_msg *msg = msgs[i];
{
// 1. 发出S信号: 设置寄存器发出S信号
CTLREG = S;
// 2. 根据Flag发出设备地址和R/W位: 把这8位数据写入某个DATAREG即可发出信号
// 判断是否有ACK
if (!ACK)
return ERROR;
else {
// 3. read/write
if (read) {
STATUS = XXX; // 这决定读到一个数据后是否发出ACK给对方
val = DATAREG; // 这会发起I2C读操作
} else if(write) {
DATAREG = val; // 这会发起I2C写操作
val = STATUS; // 判断是否收到ACK
if (!ACK)
return ERROR;
}
}
// 4. 发出P信号
CTLREG = P;
}
}
return i;
}
七. 找对应设备树节点
-
打开设备树反编译文件
cd farsight/linux-5.4.31
vi stm32mp157.dts -
在dts里找对应外设的基地址
i2c@40012000 { compatible = "st,stm32mp15-i2c"; reg = <0x40012000 0x400>; interrupt-names = "event\0error"; interrupts-extended = <0x12 0x15 0x04 0x06 0x00 0x20 0x04>; clocks = <0x0d 0x89>; resets = <0x0d 0x4c15>; #address-cells = <0x01>; #size-cells = <0x00>; dmas = <0x10 0x21 0x400 0x80000001 0x10 0x22 0x400 0x80000001>; dma-names = "rx\0tx"; power-domains = <0x11>; st,syscfg-fmp = <0x0c 0x04 0x01>; wakeup-source; status = "disabled"; }; //修改节点方法 &i2c1{ status = "okay"; si7006@40{ compatible = "stm32mp157,i2c_si7006"; reg = <0x40>; }; };
-
在datasheet里寻找对应外设的基地址
-
在设备树丛(arch/arm/boot/dts)里用管道寻找符合基地址要求的设备树
grep "reg = <0x40012000 0x400>" -rHn stm32*(-rHn显示行号和文件)
-
核对内核版本打开设备树寻找节点
十三、 USB协议
一. USB基础知识
1. USB简介
USB:Universal Serial Bus(通用串行总线)。
USB分为HOST(主机)和从机(或DEVICE),有些设备可能有时候需要做HOST,有时候又需要做DEVICE,配两个USB口当然可以实现,但是太浪费资源了。如果一个USB接口既可以做HOST又可以做DEVICE那就太好了,使用起来就方便很多。为此USBOTG应运而生,OTG是On-The-Go的缩写,支持USB OTG功能的USB接口既可以做HOST,也可以做DEVICE。
2. USB不同版本
USB版本 最大传输速率
USB 1.0 1.5Mbps(192KB/s) 低速(Low-Speed)
USB 1.1 12Mbps(1.5MB/s) 全速(Full-Speed)
USB 2.0 480Mbps(60MB/s) 高速(High-Speed)
USB 3.0 5Gbps(500MB/s) 超高速(Super-Speed)
USB 3.1 10Gbps(1280MB/s) 超高速+(Super-speed+)
1> USB2.0总线采用4芯的屏蔽线,一对差分线(D+, D-)传输信号,另一对(VBUS, 电源线) 传输+5v的直流电
针脚 名称 颜色
1 VCC 红(5V)
2 D- 白
3 D+ 绿
4 GND 黑
//USB集线器的每个下游端口D+和D-上,分别接了一个15k欧姆的下拉电阻到地。集线器悬空时,D+和D-都是低电平。而在USB设备端,D+和D-接了1.5k欧姆的上拉电阻。对于全速和高速设备,上拉电阻接在D+上;对于低速设备,上拉电阻接在D-上。这样根据差分数据线的结果就可以判断检测设备的类型。
2> USB3.0设计了9条内部线路,除了VBUS、电源线、屏蔽线之外,其余3对都是数据传输线路。其中保留了D+与D-这两条兼容USB2.0的线路,新增了SSRX与SSTX专用的USB3.0所设的线路
针脚 名称 颜色
1 VCC 红
2 D- 白
3 D+ 绿
4 GND 黑
5 SSRX- 蓝
6 SSRX+ 黄
7 GND_GRAIT 黑
8 SSTX- 紫
9 SSTX+ 橙
3> USB控制器类型
USB类型 接口标准
USB1.0 OHCI/UHCI
USB2.0 EHCI
USB3.0 xHCI
3. USB拓扑结构(hub)
USB只能主机与设备之间进行数据通信,USB主机与主机、设备与设备之间是不能通信的。因此两个正常通信的USB接口之间必定有一个主机,一个设备。为此使用了不同的插头和插座来区分主机与设备,比如主机提供USB A插座,从机提供Mini USB、Micro USB等插座。在一个USB系统中,仅有一个USB主机,但是可以有多个USB设备,包括USB功能设备和USBHUB,最多支持127个设备, USB协议中对集线器的层数是有限制的,USB1.1规定最多为4层,USB2.0规定最多为6层, 一个 USB 主控制器支持128个地址,地址0是默认地址,只有在设备枚举的时候才会使用,地址0不会分配给任何一个设备。所以一个USB主控制器最多可以分配127个地址(USB主机最多只能接127个设备,因为地址就127个,0不能用)。
虽然我们可以对原生的USB口数量进行扩展,但是我们不能对原生 USB 口的带宽进行扩展,比如两个原生USB口都是USB2.0的,带宽最大为 480Mbps,因此接到下面的所有USB设备总带宽最大为480Mbps。
4. USB传输方式
批量传输:批量传输用在那种需要大量传输数据,但对实时性要求不高的情况下。如U盘,拷贝数据的时候需要大量数据传输,但对时间上并没有特别严格的要求。
中断传输:中断传输的数据量很小,一般用于通知Host某个事件的来临,例如USB鼠标,键盘等。这里的中断并不是指硬件上那种中断机制,而是 USB 主机按照指定的时间不断的查询设备是否有数据传输。
同步传输:同步传输要保证信息传输的同步性,比如摄像头,需要实时传输数据。但同步传输也有个特点,虽然要求实时性,但不要求百分之百的包正确。
控制传输:控制传输是负责向USB设置一些控制信息。一个USB控制器下面挂接很多的设备,要怎么传输数据,还有寻址等,都是通过控制传输建立起来的
USB 主机和从机之间的通信通过管道(Pipe)来完成,管道是一个逻辑概念,任何一个 USB设备一旦上电就会存在一个管道,也就是默认管道,USB 主机通过管道来获取从机的描述符、配置等信息。在主机端管道其实就是一组缓冲区,用来存放主机数据,在设备端管道对应一个特定的端点。
5. USB设备的逻辑组织
设备:就是USB设备。
配置:每个USB设备提供不同级别的配置信息,不同配置使设备表现出不同功能, 而且可以在配置之间切换以改变设备的状态。
例如,一些允许下载 固件到其上的设备包含多个配置以完成这个工作,而一个时刻只能激活一个配置。
接口:USB接口只处理一种USB逻辑,例如鼠标,键盘或者音频流,一些USB设备具有多个接口,例如例如一个USB声卡有两个接口,一个用来播放声音,一个用来录音。因为一个USB接口代表一个基本功能,而每个USB驱动程序控制一个接口,因此,以USB声卡为例,需要两个不同的驱动程序来处理一个硬件设备。
端口:USB通信的基本形式,每一个USB设备接口在主机看来就是一个端点的集合。主机只能通过端点与设备进行通信,以使用设备的功能。
在USB系统中每一个端点都有唯一的地址。端点0通常为控制端点,用于设备初始化参数等。
查看USB设备信息
[root@fsmp1a drv_module]# lsusb -v
//root hub USB控制器
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Couldn't open device, some information will be missing
Device Descriptor://设备描述符
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 9 Hub//设备类型
bDeviceSubClass 0 Unused//具体设备
bDeviceProtocol 0 Full speed (or root) hub\\高速还是低速类型设备
bMaxPacketSize0 64
idVendor 0x1d6b Linux Foundation
idProduct 0x0002 2.0 root hub
bcdDevice 5.03
iManufacturer 3
iProduct 2
iSerial 1
bNumConfigurations 1//配置个数
Configuration Descriptor://配置描述符
bLength 9
bDescriptorType 2
wTotalLength 25
bNumInterfaces 1//接口数量只有一个
bConfigurationValue 1
iConfiguration 0
bmAttributes 0xe0//配置属性:供电模式
Self Powered
Remote Wakeup
MaxPower 0mA
Interface Descriptor://接口描述符
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1//端点数量1个
bInterfaceClass 9 Hub//类型是集线器
bInterfaceSubClass 0 Unused
bInterfaceProtocol 0 Full speed (or root) hub//高速模式hub
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN//端点地址0x81 端点号:1 输入模式
bmAttributes 3//端口属性。0表示控制,1表示等时,2表示批量,3表示中断
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0004 1x 4 bytes//端点一次传送的最多字节
bInterval 12//主控制器轮询设备的间隔
二. USB协议
1. USB描述符(重点,面试都要问的)
USB 描述符就是用来描述 USB 信息的,描述符就是一串按照一定规则构建的字符串,USB 设备使用描述符来向主机报告自己的相关属性信息。
设备描述符 -> struct usb_device_descriptor
配置描述符 -> struct usb_config_descriptor
端口描述符 -> struct usb_interface_descriptor
端点描述符 -> struct usb_endpoint_descriptor
1> 设备描述符
设备描述符用于描述 USB 设备的一般信息,USB 设备只有一个设备描述符。设备描述符里面记录了设备的 USB 版本号、设备类型、VID(厂商 ID)、PID(产品 ID)、设备序列号等。
2> 配置描述符
设备描述符的 bNumConfigurations 域定义了一个 USB 设备的配置描述符数量,一个 USB设备至少有一个配置描述符。配置描述符描述了设备可提供的接口(Interface)数量、配置编号、供电信息等。
3> 字符串描述符
字符串描述符是可选的,字符串描述符用于描述一些方便人们阅读的信息,比如制造商、设备名称啥的。如果一个设备没有字符串描述符,那么其他描述符中和字符串有关的索引值都必须为 0。
4> 接口描述符
配置描述符中指定了该配置下的接口数量,配置可以提供一个或多个接口,接口描述符用于描述接口属性。接口描述符中一般记录接口编号、接口对应的端点数量、接口所述的类等。
5> 端口描述符
接口描述符定义了其端点数量,端点是设备与主机之间进行数据传输的逻辑接口,除了端点0是双向端口,其他的端口都是单向的。端点描述符描述了树传输类型、方向、数据包大小、端点号等信息。
2、USB描述符结构体(全在USB设备的EEPROM里)
struct usb_device_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__u16 bcdUSB;
__u8 bDeviceClass;
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
__u8 bMaxPacketSize0;
__u16 idVendor;
__u16 idProduct;
__u16 bcdDevice;
__u8 iManufacturer;
__u8 iProduct;
__u8 iSerialNumber;
__u8 bNumConfigurations;//表示设备里有几个配置
} __attribute__ ((packed));
struct usb_config_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__le16 wTotalLength;
__u8 bNumInterfaces;//表示配置里有几个接口,一个接口表示一个功能
__u8 bConfigurationValue;
__u8 iConfiguration;
__u8 bmAttributes;
__u8 bMaxPower;
} __attribute__ ((packed));
struct usb_interface_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__u8 bInterfaceNumber;
__u8 bAlternateSetting;
__u8 bNumEndpoints;//表示接口里有几个端点,一个端点表示一种通信方式
__u8 bInterfaceClass;
__u8 bInterfaceSubClass;
__u8 bInterfaceProtocol;
__u8 iInterface;
} __attribute__ ((packed));
struct usb_endpoint_descriptor {
__u8 bLength;
__u8 bDescriptorType;//描述符类型,对于端点就是USB_dt_ENDPOINT
__u8 bEndpointAddress;//它的bits 0~3表示的就是端点地址,bit 8表示方向,输入还是输出
__u8 bmAttributes;//表示属性,其中bit1和bit0共同称为Transfer Type即传输类型,
//00表示控制传输(主机获取/配置从设备),01表示等时传输(传输大批量数据,只管发,实时性比较高例如音频、视频、USB摄像头,话筒),
//10表示批量传输(U盘),11表示中断传输(鼠标)
__le16 wMaxPacketSize;
__u8 bInterval;//希望主机轮询自己的时间间隔
/* NOTE: these two are _only_ in audio endpoints. */
/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
__u8 bRefresh;
__u8 bSynchAddress;
} __attribute__ ((packed));
3. USB数据包类型
USB是串行通信,需要一位一位的去传输数据,USB传输的时候先将原始数据进行打包,所以USB中传输的基本单元就是数据包。根据用途的不同,USB 协议定义了4种不同的包结构:令牌(Token)包、数据(Data)包、握手(Handshake)包和特殊(Special)包。这四种包通过包标识符PID来区分,PID 共有 8 位,USB 协议使用低 4 位 PID3~PID0,另外的高四位 PID7~PID4 是PID3~PID0 的取反,传输顺序是 PID0、PID1、PID2、PID3…PID7。令牌包的 PID1~0 为 01,数据包的 PID1~0 为 11,握手包的 PID1~0 为 10,特殊包的 PID1~0 为 00。每种类型的包又有多种具体的包。
一个完整的包分为多个域,所有的数据包都是以同步域(SYNC)开始,后面紧跟着包标识符(PID),最终都以包结束(EOP)信号结束。不同的数据包中间位域不同,一般有包目标地址(ADDR)、包目标端点(ENDP)、数据、帧索引、CRC 等,这个要具体数据包具体分析。接下来简单看一下这些数据包的结构。
1> 令牌包
上图是一个 SETUP 令牌包结构,首先是 SYNC 同步域,包同步域为 00000001,也就是连续 7 个 0,后面跟一个 1,如果是高速设备的话就是 31 个 0 后面跟一个 1。紧跟着是 PID,这里是 SETUP 包,为 0XB4,大家可能会好奇为什么不是 0X2D(00101101),0XB4 的原因如下:
①、SETUP 包的 PID3~PID0 为 1101,因此对应的 PID7~PID4 就是 0010。
②、PID 传输顺序为 PID0、PID1、PID2…PID7,因此按照传输顺序排列的话此处的 PID 就是 10110100=0XB4,并不是 0X2D。PID 后面跟着地址域(ADDR)和端点域(ENDP),为目标设备的地址和端点号。CRC5 域是 5位 CRC 值,是 ADDR 和 ENDP 这两个域的校验值。最后就是包结束域(EOP),标记本数据包结束。其他令牌包的结构和 SETUP 基本类似,只是 SOF 令包中间没有 ADDR 和 ENDP 这两个域,而是只有一个 11 位的帧号域。
2> 数据包
数据包比较简单,同样的,数据包从 SYNC 同步域开始,然后紧跟着是 PID,这里就是DATA0,PID 值为 0XC3。接下来就是具体的数据,数据完了以后就是 16 位的 CRC 校验值,最后是 EOP。
3> 握手包
首先是 SYNC 同步域,然后就是 ACK 包的 PID,为0X4B,最后就是 EOP。其他的 NAK、STALL、NYET 和 ERR 握手包结构都是一样的,只是其中的 PID 不同而已。
4. USB传输类型
1> 控制传输
控制传输一般用于特定的请求,比如枚举过程就是全部由控制传输来完成的,比如获取描述符、设置地址、设置配置等。控制传输分为三个阶段:建立阶段(SETUP)、数据阶段(DATA)和状态阶段(STATUS),其中数据阶段是可选的。建立阶段使用 SETUP 令牌包,SETUP 使用DATA0 包。数据阶段是 0 个、1 个或多个输入(IN)/输出(OUT)事务,数据阶段的所有输入事务必须是同一个方向的,比如都为 IN 或都为 OUT。数据阶段的第一个数据包必须是 DATA1,每次正确传输以后就在 DATA0 和 DATA1 之间进行切换。数据阶段完成以后就是状态阶段,状态阶段的传输方向要和数据阶段相反,比如数据阶段为 IN 的话状态阶段就要为 OUT,状态阶段使用 DATA1 包。
2> 同步传输
同步传输用于周期性、低时延、数据量大的场合,比如音视频传输,这些场合对于时延要求很高,但是不要求数据 100%正确,允许有少量的错误。因此,同步传输没有握手阶段,即使数据传输出错了也不会重传。
3> 批量传输
提起“批量”,我们第一反应就是“多”、“大”等,因此,批量传输就是用于大批量传输大块数据的,这些数据对实时性没有要求,比如 MSD 类设备(存储设备),U 盘之类的。批量传输分为批量读(输入)和批量写(输出),如果是批量读的话第一阶段就是 IN 令牌包,如果是批量写那么第一阶段就是 OUT 令牌包。我们就以批量写为例简单介绍一下批量传输过程:
①、主机发出 OUT 令牌包,令牌包里面包含了设备地址、端点等信息。
②、如果 OUT 令牌包正确的话,也就是设备地址和端点号匹配,主机就会向设备发送一个数据(DATA)包,发送完成以后主机进入接收模式,等待设备返回握手包,一切都正确的话设备就会向主机返回一个 ACK 握手信号。
批量读的过程刚好相反:
①、主机发出 IN 令牌包,令牌包里面包含了设备地址、端点等信息。发送完成以后主机就进入到数据接收状态,等待设备返回数据。
②、如果 IN 令牌包正确的话,设备就会将一个 DATA 包放到总线上发送给主机。主机收到这个 DATA 包以后就会向设备发送一个 ACK 握手信号。
4> 中断传输
这里的中断传输并不是我们传统意义上的硬件中断,而是一种保持一定频率的传输,中断传输适用于传输数据量小、具有周期性并且要求响应速度快的数据,比如键盘、鼠标等。中断的端点会在端点描述符中报告自己的查询时间间隔,对于时间要求严格的设备可以采用中断传输。
5.USB驱动架构
6.主控制器驱动功能
1、解析和维护URB(请求申请块)
2、负责不同USB传输类型的调度工作
3、负责USB数据的实际传输工作
4、实现虚拟HUB的功能
5、平台驱动层相关代码路径/home/farsight/farsight/linux-5.4.31/drivers/usb/host/ehci-s5p.c
//部分源码
static struct platform_driver s5p_ehci_driver = {
.probe = s5p_ehci_probe,
.remove = __devexit_p(s5p_ehci_remove),
.shutdown = s5p_ehci_shutdown,
.driver = {
.name = "s5p-ehci",//注册了s5p-ehci这个设备平台驱动才会工作(probe才会动)
.owner = THIS_MODULE,
}
};
6、平台设备层注册的路径(linux-3.0.8内核):/home/farsight/farsight/linux-3.0.8/arch/arm
7、平台设备层路径下寻找相关平台设备:grep s5p-ehci -r *(以s5p-ehci为例子)
farsight@ubuntu:~/farsight/linux-3.0.8/arch/arm$ grep s5p-ehci -r *
plat-s5p/dev-ehci.c: .name = "s5p-ehci",
8、部分源码:
/* USB EHCI Host Controller registration */
static struct resource s5p_ehci_resource[] = {
[0] = {
.start = S5P_PA_EHCI,
.end = S5P_PA_EHCI + SZ_256 - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_USB_HOST,
.end = IRQ_USB_HOST,
.flags = IORESOURCE_IRQ,
}
};
static u64 s5p_device_ehci_dmamask = 0xffffffffUL;
struct platform_device s5p_device_ehci = {
.name = "s5p-ehci",
.id = -1,
.num_resources = ARRAY_SIZE(s5p_ehci_resource),
.resource = s5p_ehci_resource,
.dev = {
.dma_mask = &s5p_device_ehci_dmamask,
.coherent_dma_mask = 0xffffffffUL
}
};
7.端点endpoint
1、它是USB通信最基本的形式
2、每个端点只能往一个方向传输数据,IN/OUT,端点0除外(端点0是双工的)
3、端点存在于USB设备端
4、主机和端点之间的数据传输要通过管道
8.请求块urb
struct urb {//urb是主机发给从机的请求块
/* private: usb core and host controller only fields in the urb */
struct kref kref; /* reference count of the URB */
int unlinked; /* unlink error code */
void *hcpriv; /* private data for host controller */
atomic_t use_count; /* concurrent submissions counter */
atomic_t reject; /* submissions will fail */
/* public: documented fields in the urb that can be used by drivers */
struct list_head urb_list; /* list head for use by the urb's
* current owner */
struct list_head anchor_list; /* the URB may be anchored */
struct usb_anchor *anchor;
struct usb_device *dev; /* (in) pointer to associated device */
struct usb_host_endpoint *ep; /* (internal) pointer to endpoint */
unsigned int pipe; /* (in) pipe information */
//主机想要联系对应端点的管道号
unsigned int stream_id; /* (in) stream ID */
int status; /* (return) non-ISO status */
unsigned int transfer_flags; /* (in) URB_SHORT_NOT_OK | ...*/
void *transfer_buffer; /* (in) associated data buffer */
//如果是IN,transfer_buffer里面存从机要发送给主机的数据;如果是OUT,transfer_buffer里存要发给从机的数据
dma_addr_t transfer_dma; /* (in) dma addr for transfer_buffer */
struct scatterlist *sg; /* (in) scatter gather buffer list */
int num_mapped_sgs; /* (internal) mapped sg entries */
int num_sgs; /* (in) number of entries in the sg list */
u32 transfer_buffer_length; /* (in) data buffer length */
u32 actual_length; /* (return) actual transfer length */
unsigned char *setup_packet; /* (in) setup packet (control only) */
dma_addr_t setup_dma; /* (in) dma addr for setup_packet */
int start_frame; /* (modify) start frame (ISO) */
int number_of_packets; /* (in) number of ISO packets */
int interval; /* (modify) transfer interval 轮询间隔
* (INT/ISO) */
int error_count; /* (return) number of ISO errors */
void *context; /* (in) context for completion */
//用于保存urb上下文
usb_complete_t complete; /* (in) completion routine */
//传输过程中,当urb的传输是异步操作的时候,主机将urb发送出去后,如果从机给予回应了主机需要的信息,就会执行complete函数
struct usb_iso_packet_descriptor iso_frame_desc[0];
/* (in) ISO ONLY */
};
1、分配urb
extern struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);
2、初始化urb
/*
* usb_fill_control_urb - initializes a control urb
* @urb: pointer to the urb to initialize.
* @dev: pointer to the struct usb_device for this urb.
* @pipe: the endpoint pipe
* @setup_packet: pointer to the setup_packet buffer
* @transfer_buffer: pointer to the transfer buffer
* @buffer_length: length of the transfer buffer
* @complete_fn: pointer to the usb_complete_t function
* @context: what to set the urb context to.
*
* Initializes a control urb with the proper information needed to submit
* it to a device.
*/
//初始化控制/中断/批量传输urb
static inline void usb_fill_control|int||bulk|_urb(struct urb *urb,
struct usb_device *dev,
unsigned int pipe,
unsigned char *setup_packet,
void *transfer_buffer,
int buffer_length,
usb_complete_t complete_fn,
void *context)
{
urb->dev = dev;
urb->pipe = pipe;
urb->transfer_buffer = transfer_buffer;
urb->transfer_buffer_length = buffer_length;
urb->complete = complete_fn;
urb->context = context;
if (dev->speed == USB_SPEED_HIGH || dev->speed >= USB_SPEED_SUPER) {
/* make sure interval is within allowed range */
interval = clamp(interval, 1, 16);
urb->interval = 1 << (interval - 1);
} else {
urb->interval = interval;
}
urb->start_frame = -1;
}
3、提交urb(提交给主控制器,由主控制器发送给usb设备)
(1)异步提交urb,提交完成后执行usb_fill[control|int|bulk]_urb传入的回调函数
int usb_submit_urb(struct urb *urb, gfp_t mem_flags)
(2)同步提交urb,得到从机回应才会触发
int usb_[control|interrupt|bulk]_msg(struct usb_device *dev, unsigned int pipe, __u8 request,
__u8 requesttype, __u16 value, __u16 index, void *data,
__u16 size, int timeout)
{
struct usb_ctrlrequest *dr;
int ret;
dr = kmalloc(sizeof(struct usb_ctrlrequest), GFP_NOIO);
if (!dr)
return -ENOMEM;
dr->bRequestType = requesttype;
dr->bRequest = request;
dr->wValue = cpu_to_le16(value);
dr->wIndex = cpu_to_le16(index);
dr->wLength = cpu_to_le16(size);
ret = usb_internal_control_msg(dev, pipe, dr, data, size, timeout);
/* Linger a bit, prior to the next control message. */
if (dev->quirks & USB_QUIRK_DELAY_CTRL_MSG)
msleep(200);
kfree(dr);
return ret;
}
9.管道
每个端点通过管道和usb主控制器连接,管道包括以下几个部分:
1、端点地址
2、数据传输方向(IN或OUT)
3、数据传输模式
//通过端点来得到管道
pipe = usb_[rcv|snd][ctrl|int|bulk|isoc]intpipe(
struct usb_device *usb_dev,
__u8 endpointAddress
);
10.驱动函数
struct usb_driver {
const char *name;
int (*probe) (struct usb_interface *intf,
const struct usb_device_id *id);
void (*disconnect) (struct usb_interface *intf);
int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code,
void *buf);
int (*suspend) (struct usb_interface *intf, pm_message_t message);
int (*resume) (struct usb_interface *intf);
int (*reset_resume)(struct usb_interface *intf);
int (*pre_reset)(struct usb_interface *intf);
int (*post_reset)(struct usb_interface *intf);
const struct usb_device_id *id_table;//用于匹配USB的注册设备启动probe
const struct attribute_group **dev_groups;
struct usb_dynids dynids;
struct usbdrv_wrap drvwrap;
unsigned int no_dynamic_id:1;
unsigned int supports_autosuspend:1;
unsigned int disable_hub_initiated_lpm:1;
unsigned int soft_unbind:1;
};
-------------------------------------------------------------------------------------------
struct usb_device_id {
/* which fields to match against? */
__u16 match_flags;
/* Used for product specific matches; range is inclusive */
__u16 idVendor;
__u16 idProduct;
__u16 bcdDevice_lo;
__u16 bcdDevice_hi;
/* Used for device class matches */
__u8 bDeviceClass;//USB设备的分类,例如USB鼠标和USB键盘都是IN设备;USB摄像头就是其它类的设备
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
/* Used for interface class matches */
__u8 bInterfaceClass;
__u8 bInterfaceSubClass;
__u8 bInterfaceProtocol;
/* Used for vendor-specific interface matches */
__u8 bInterfaceNumber;
/* not matched against */
kernel_ulong_t driver_info
__attribute__((aligned(sizeof(kernel_ulong_t))));
};
----------------------------------------------------------------------------------------------
MODULE_DEVICE_TABLE (usb, usb_mouse_id_table);
//3个接口的话就要3个接口驱动,一个接口对应一种驱动功能
static struct usb_driver usb_mouse_driver = {
.name = "usbmouse",
.probe = usb_mouse_probe,
//设备拔出的时候执行的函数
.disconnect = usb_mouse_disconnect,
.id_table = usb_mouse_id_table,
};
-----------------------------------------------------------------------------------------------
//USB_INTERFACE_INFO:本主机支持匹配的USB设备类型
//HID(Human Interface Device)人体接口设备
//USB_INTERFACE_SUBCLASS_BOOT:具体设备:键鼠
//USB_INTERFACE_PROTOCOL_MOUSE:高速设备或者低速设备
static const struct usb_device_id usb_mouse_id_table[] = {
{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,
USB_INTERFACE_PROTOCOL_MOUSE) },
{ } /* Terminating entry */
};
三、USB鼠标驱动编程思路
1. 注册USB接口
static int __init usb_mouse_init(void)
{
//这不是一个USB设备的driver而是一个接口的driver
//注册之后产生一个dev来匹配和总线匹配之后调用probe函数
int retval = usb_register(&usb_mouse_driver);
if(retval == 0)
{
printk(KERN_INFO KBUILD_MODNAME":" DRIVER_VERSION":"
DRIVER_DESC"\n");
}
return retval;
}
2. 上一条反操作
static int __init usb_mouse_exit(void)
{
usb_deregister(&usb_mouse_driver);
}
3. 自定义mouse结构体,定义总线,定义驱动结构体
struct usb_mouse {
char name[128];
char phys[64];
//用来继承的usb设备结构体
struct usb_device *usbdev;
//输入驱动设备结构体
struct input_dev *dev;
//USB请求块,即主控制器与USB设备的通信协议
struct urb *irq;
signed char *data;
//DMA操作
dma_addr_t data_dma;
};
//USB_INTERFACE_INFO:本主机支持匹配的USB设备类型
//HID(Human Interface Device)人体接口设备
//USB_INTERFACE_SUBCLASS_BOOT:具体设备:键鼠
//USB_INTERFACE_PROTOCOL_MOUSE:高速设备或者低速设备
static const struct usb_device_id usb_mouse_id_table[] = {
{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,
USB_INTERFACE_PROTOCOL_MOUSE) },
{ } /* Terminating entry */
};
MODULE_DEVICE_TABLE (usb, usb_mouse_id_table);
//3个接口的话就要3个接口驱动
static struct usb_driver usb_mouse_driver = {
.name = "usbmouse",
.probe = usb_mouse_probe,
//设备拔出的时候执行的函数
.disconnect = usb_mouse_disconnect,
.id_table = usb_mouse_id_table,
};
4. 总线匹配完成进入probe初始化设备、接口、端口、mouse、input
static int usb_mouse_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
//USB设备结构体
struct usb_device *dev = interface_to_usbdev(intf);
//USB接口配置结构体
struct usb_host_interface *interface;
//USB端口结构体
struct usb_endpoint_descriptor *endpoint;
//自定义鼠标结构体
struct usb_mouse *mouse;
struct input_dev *input_dev;
int pipe, maxp;
int error = -ENOMEM;
//鼠标初始化
//通过接口获得当前设置
interface = intf->cur_altsetting;
//desc为接口描述符
if (interface->desc.bNumEndpoints != 1)
return -ENODEV;
//得到端点描述符的结构体
endpoint = &interface->endpoint[0].desc;
//判断端点是不是in端点
if (!usb_endpoint_is_int_in(endpoint))
return -ENODEV;
//通过端点来得到管道
pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
//获得主控制器一次传输的最大字节数(封包数)是多少
maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe));
mouse = kzalloc(sizeof(struct usb_mouse), GFP_KERNEL);
//申请一个input子系统框架
input_dev = input_allocate_device();
if (!mouse || !input_dev)
goto fail1;
//给data申请空间用于DMA传输
mouse->data = usb_alloc_coherent(dev, 8, GFP_ATOMIC, &mouse->data_dma);
if (!mouse->data)
goto fail1;
//给USB申请块申请空间
mouse->irq = usb_alloc_urb(0, GFP_KERNEL);
if (!mouse->irq)
goto fail2;
mouse->usbdev = dev;
mouse->dev = input_dev;
//厂商
if (dev->manufacturer)
strlcpy(mouse->name, dev->manufacturer, sizeof(mouse->name));
//产品
if (dev->product) {
if (dev->manufacturer)
strlcat(mouse->name, " ", sizeof(mouse->name));
strlcat(mouse->name, dev->product, sizeof(mouse->name));
}
if (!strlen(mouse->name))
snprintf(mouse->name, sizeof(mouse->name),
"USB HIDBP Mouse %04x:%04x",
le16_to_cpu(dev->descriptor.idVendor),
le16_to_cpu(dev->descriptor.idProduct));
//分配路径
usb_make_path(dev, mouse->phys, sizeof(mouse->phys));
strlcat(mouse->phys, "/input0", sizeof(mouse->phys));
input_dev->name = mouse->name;
//系统层次结构中设备的物理路径
input_dev->phys = mouse->phys;
//改厂商和产品版本的东西
usb_to_input_id(dev, &input_dev->id);
input_dev->dev.parent = &intf->dev;
//初始化鼠标Input输入结构体对象(按键+相对坐标)
input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REL);
//初始化鼠标的前中后按键位(左键、右键、滚轮键)
input_dev->keybit[BIT_WORD(BTN_MOUSE)] = BIT_MASK(BTN_LEFT) |
BIT_MASK(BTN_RIGHT) | BIT_MASK(BTN_MIDDLE);
//初始化x,y坐标
input_dev->relbit[0] = BIT_MASK(REL_X) | BIT_MASK(REL_Y);
//初始化鼠标的侧位键
input_dev->keybit[BIT_WORD(BTN_MOUSE)] |= BIT_MASK(BTN_SIDE) |
BIT_MASK(BTN_EXTRA);
//初始化滚轮
input_dev->relbit[0] |= BIT_MASK(REL_WHEEL);
//数据从mouse复制到input_dev的data变量里
input_set_drvdata(input_dev, mouse);
//调用input子系统里open和close接口
input_dev->open = usb_mouse_open;
input_dev->close = usb_mouse_close;
//初始化urb,当USB设备讲数据放到mouse->data之后就调用完成函数usb_mouse_irq上报
usb_fill_int_urb(mouse->irq, dev, pipe, mouse->data,
(maxp > 8 ? 8 : maxp),
usb_mouse_irq, mouse, endpoint->bInterval);
mouse->irq->transfer_dma = mouse->data_dma;
mouse->irq->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
//注册input_dev对象
error = input_register_device(mouse->dev);
if (error)
goto fail3;
usb_set_intfdata(intf, mouse);
return 0;
fail3:
usb_free_urb(mouse->irq);
fail2:
usb_free_coherent(dev, 8, mouse->data, mouse->data_dma);
fail1:
input_free_device(input_dev);
kfree(mouse);
return error;
}
5. 打开设备节点进行异步提交urb
static int usb_mouse_open(struct input_dev *dev)
{
struct usb_mouse *mouse = input_get_drvdata(dev);
mouse->irq->dev = mouse->usbdev;
//异步提交urb给主控制器,请求主控制器将urb发送给相应的主控制器设备
if (usb_submit_urb(mouse->irq, GFP_KERNEL))
return -EIO;
return 0;
}
6. 反操作
static void usb_mouse_close(struct input_dev *dev)
{
struct usb_mouse *mouse = input_get_drvdata(dev);
usb_kill_urb(mouse->irq);
}
7. 事件驱动提交数据
static void usb_mouse_irq(struct urb *urb)
{
//定位到那个设备的上下文
struct usb_mouse *mouse = urb->context;
//***设备回应给主控制器的信息
signed char *data = mouse->data;
//得到鼠标input的指针
struct input_dev *dev = mouse->dev;
//获得设备回过来的urb状态,0才是正确的状态
int status;
switch (urb->status) {
case 0: /* success */
break;
case -ECONNRESET: /* unlink */
case -ENOENT:
case -ESHUTDOWN:
return;
/* -EPIPE: should clear the halt */
default: /* error */
goto resubmit;
}
//鼠标数据格式:第0个bit是置1的就代表左键按下,下面同理进行事件驱动。
input_report_key(dev, BTN_LEFT, data[0] & 0x01);
input_report_key(dev, BTN_RIGHT, data[0] & 0x02);
input_report_key(dev, BTN_MIDDLE, data[0] & 0x04);
input_report_key(dev, BTN_SIDE, data[0] & 0x08);
input_report_key(dev, BTN_EXTRA, data[0] & 0x10);
input_report_rel(dev, REL_X, data[1]);
input_report_rel(dev, REL_Y, data[2]);
input_report_rel(dev, REL_WHEEL, data[3]);
//表示上面的一套数据是一帧数据,即上面的事件是一个完整的事件,一次信息传送完成
input_sync(dev);
//下面是轮询发送数据
resubmit:
status = usb_submit_urb (urb, GFP_ATOMIC);
if (status)
dev_err(&mouse->usbdev->dev,
"can't resubmit intr, %s-%s/input0, status %d\n",
mouse->usbdev->bus->bus_name,
mouse->usbdev->devpath, status);
}
static int usb_mouse_open(struct input_dev *dev)
{
struct usb_mouse *mouse = input_get_drvdata(dev);
mouse->irq->dev = mouse->usbdev;
//异步提交urb给主控制器,请求主控制器将urb发送给相应的主控制器设备
if (usb_submit_urb(mouse->irq, GFP_KERNEL))
return -EIO;
return 0;
}
四. 设备树
USB子系统是一个标准和复杂的接口,所以驱动基本我们不用写,都是内核里有现成的,我们只需要在设备树提供对应的设备节点即可。
USBH是只能主机模式,要编写USB HOST就用USBH控制器即可。
ST官方的STM3MP157开发板已经配置好了USBH的节点信息,所以我们直接参考此节点即可,这样开发板就能够使用USB Host模式。
1. USBH 控制器节点信息
打开“stm32mp151.dtsi”文件,找到 USBH 两个控制器的节点信息,名字分别为“usbh_ohci” 和“usbh_ehci”。如下示例代码所示:
usbh_ohci: usbh-ohci@5800c000 {
compatible = "generic-ohci";
reg = <0x5800c000 0x1000>;
clocks = <&rcc USBH>;
resets = <&rcc USBH_R>;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>;
status = "disabled";
};
usbh_ehci: usbh-ehci@5800d000 {
compatible = "generic-ehci";
reg = <0x5800d000 0x1000>;
clocks = <&rcc USBH>;
resets = <&rcc USBH_R>;
interrupts-extended = <&exti 43 IRQ_TYPE_LEVEL_HIGH>;
companion = <&usbh_ohci>;
power-domains = <&pd_core>;
wakeup-source;
status = "disabled";
};
从上面的代码可以知道 USBH 是支持 USB2.0 和 USB1.1。使用 USB2.0 就要配置 usbh_ehci 节点,使用 USB1.1 就要配置 usbh_ohci 节点。这两个节点的信息我们是不需要修改的,这是 STM32MP1 一些通用配置信息。需要做的就是在 stm32mp157a-fsmp157.dts 文件中追加对应属性信 息,然后 status 属性值改为“okay”,还要配置 USBH 使用哪个 PHY 端口。根据两个节点的 compatible 属性找到对应的驱动路径为:drivers/usb/host/ohci-platform.c 和 drivers/usb/host/ehciplatform.c
2. 配置 PHY 控制器
我们先去了解一下 PHY 控制器,一些通用配置,打开 stm32mp151.dtsi 文件,找的如下内 容所示:
usbphyc: usbphyc@5a006000 {
#address-cells = <1>;
#size-cells = <0>;
#clock-cells = <0>;
compatible = "st,stm32mp1-usbphyc";
reg = <0x5a006000 0x1000>;
clocks = <&rcc USBPHY_K>;
resets = <&rcc USBPHY_R>;
vdda1v1-supply = <®11>;
vdda1v8-supply = <®18>;
status = "disabled";
usbphyc_port0: usb-phy@0 {
#phy-cells = <0>;
reg = <0>;
};
usbphyc_port1: usb-phy@1 {
#phy-cells = <1>;
reg = <1>;
};
};
usbphyc 节点就是 STM32MP1 的 USB PHY。我已经知道 PHY 控制器有两个端口,刚好 usbphyc 节点里有两个子节点名字分别为:usbphyc_port0 和 usbphyc_port1,这个两个子节点就是 PHY 控制器的两个端口,其中 usbphyc_port0 只能分配给 USB Host。注意:
“#phy-cells”属性和“#gpio-cells”属性作用是一样的,如果#phy-cells 为 1,表示一个 cell,此cell 表示端口做 USBH 的 PHY 端口还是 OTG 的 PHY 端口,0 表示做 OTG 的 PHY 端口,1 表示做 USBH 的 PHY 端口
打开“stm32mp15xx-dkx.dtsi”文件找到“usb_phy_tuning”节点,把此节点的内容拷贝到 stm32mp157a-fsmp157.dts 的根目录下,拷贝内容如下所示:
usb_phy_tuning: usb-phy-tuning {
st,hs-dc-level = <2>;
st,fs-rftime-tuning;
st,hs-rftime-reduction;
st,hs-current-trim = <15>;
st,hs-impedance-trim = <1>;
st,squelch-level = <3>;
st,hs-rx-offset = <2>;
st,no-lsfs-sc;
};
usb_phy_tuning 此节点负责调整 PHY 的配置,对于此节点的属性内容感兴趣的可以去看
Documentation/devicetree/bindings/phy/phy-stm32-usbphyc.yaml 文 件
接 着 我 还 是 在 stm32mp157a-fsmp157.dts 文件中使能 usbphyc 以及向 usbphyc_port0 节点追加的内容,要修改的如下所示:
&usbphyc {
status = "okay";
};
&usbphyc_port0 {
phy-supply = <&v3v3>;
st,phy-tuning = <&usb_phy_tuning>;
};
第 2 行,这里我们把 usbphyc 的 status 属性修改为“okay”,使能 usbphyc。
第 6 行,给 usbphyc_port0 节点追加 phy-supply 属性,添加一个电源管理属性。
第 7 行,把我们要修改的 PHY 配置,添加到 usbphyc_port0 节点里。
最后我们在 stm32mp157d-atk.dts 文件,使能 usbh_ehci 和指定 PHY 端口:
&usbh_ehci {
phys = <&usbphyc_port0>;
status = "okay";
};
  这里的内容很简单,就是修改 status 属性为“okay”和指定使用 usbphyc_port0 端口。
十四、SPI协议
一.SPI硬件知识
1. 硬件连接
引脚 | 含义 |
---|---|
DO(MOSI) | Master Output, Slave Input, SPI主控用来发出数据,SPI从设备用来接收数据 |
DI(MISO) | Master Input, Slave Output, SPI主控用来发出数据,SPI从设备用来接收数据 |
SCK | Serial Clock,时钟 |
CS | Chip Select,芯片选择引脚 |
2. 传输示例
假设现在主控芯片要传输一个0x56数据给SPI Flash,时序如下:
首先CS0先拉低选中SPI Flash,0x56的二进制就是0b0101 0110,因此在每个SCK时钟周期,DO输出对应的电平。
SPI Flash会在每个时钟周期的上升沿读取D0上的电平。
3. SPI模式
在SPI协议中,有两个值来确定SPI的模式。
CPOL:表示SPICLK的初始电平,0为电平,1为高电平。
CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿。
| CPOL | CPHA | 模式 | 含义|
| 0 | 0 | 0 | SPICLK初始电平为低电平,在第一个时钟沿采样数据 |
| 0 | 1 | 1 | SPICLK初始电平为低电平,在第二个时钟沿采样数据 |
| 1 | 0 | 2 | SPICLK初始电平为高电平,在第一个时钟沿采样数据 |
| 1 | 1 | 3 | SPICLK初始电平为高电平,在第二个时钟沿采样数据 |
我们常用的是模式0和模式3,因为它们都是在上升沿采样数据,不用去在乎时钟的初始电平是什么,只要在上升沿采集数据就行。
极性选什么?格式选什么?通常去参考外接的模块的芯片手册。比如对于OLED,查看它的芯片手册时序部分:
对于下图而言,SCLK的初始电平我们并不需要关心,只要保证在上升沿采样数据就行。
二. Linux SPI驱动框架
应用层:APP
-----------------------------------------------------
驱动层:
spi_driver层:从设备驱动层<自己实现>
//初始化从设备
//和用户进行交互
--------------------------------------------------
spi核心层:linux-5.4.31/drivers/spi/spi.c
//维护spi虚拟总线框架
--------------------------------------------------
spi_master层:linux-5.4.31/drivers/spi/spi-stm32.c
//初始化SPI控制器
//实现数据收发的方法
三. SPI从设备驱动
确保spi.c和spi-stm32.c被编译到内核。
Device Drivers --->
[*]SPI support --->
<*>STMicroelectronics STM32 SPI controller
编写代码
1> 重要结构体
struct spi_transfer {
const void *tx_buf;
void *rx_buf;
unsigned len;
};
struct spi_message {
struct list_head transfers;
struct spi_device *spi;
unsigned is_dma_mapped:1;
void (*complete)(void *context);
void *context;
unsigned frame_length;
unsigned actual_length;
int status;
struct list_head queue;
void *state;
struct list_head resources;
};
2> 方法
int spi_send_data(struct spi_device * spi, unsigned short data)
{
int error;
struct spi_message message;
struct spi_transfer xfer[1] = {0};
unsigned char tx[2] = {0};
tx[0] = data & 0xff;
tx[1] = code[data >> 8];
spi_message_init(&message);
xfer[0].tx_buf = (const void *)&tx;
xfer[0].len = sizeof(tx)/sizeof(tx[0]);
spi_message_add_tail(&xfer[0], &message);
error = spi_sync(spi, &message);
if (unlikely(error))
{
dev_err(&spi->dev, "SPI read error: %d\n", error);
return error;
}
return 0;
}
设备树
设备树内核源码例子:/home/farsight/farsight/linux-5.4.31/Documentation/devicetree/bindings
修改linux-5.4.31/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts,增加如下节点:
&spi4 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&spi4_pins_a>;//默认情况下的GPIO属性
//决定引用哪组定义好的GPIO。可通过命令grep 'spi4_sleep_pins_a' -rHn *查看源码情况。
pinctrl-1 = <&spi4_sleep_pins_a>;//"sleep"情况下的GPIO属性
cs-gpios = <&gpioe 11 GPIO_ACTIVE_HIGH>;//片选
status = "okay";
m74hc595: m74hc595@0 {
compatible = "stm32mp157,spi_m74hc595";
reg = <0>;
spi-max-frequency = <20000000>;//看数据手册的1/周期算频率,这里是20M.
};
};
//grep 'spi4_sleep_pins_a' -rHn *查看源码情况
&pinctrl {
u-boot,dm-pre-reloc;
//default情况下的GPIO属性(完全复用)
spi4_pins_mx: spi4_mx-0 {
pins {
pinmux = <STM32_PINMUX('E', 12, AF5)>, /* SPI4_SCK */
<STM32_PINMUX('E', 13, AF5)>, /* SPI4_MISO */
<STM32_PINMUX('E', 14, AF5)>; /* SPI4_MOSI */
bias-disable;
drive-push-pull;
slew-rate = <1>;
};
};
//sleep情况下的GPIO属性
spi4_sleep_pins_mx: spi4_sleep_mx-0 {
pins {
pinmux = <STM32_PINMUX('E', 12, ANALOG)>, /* SPI4_SCK */
<STM32_PINMUX('E', 13, ANALOG)>, /* SPI4_MISO */
<STM32_PINMUX('E', 14, ANALOG)>; /* SPI4_MOSI */
};
};
/* USER CODE BEGIN pinctrl */
/* USER CODE END pinctrl */
};
四. SPI_OLED模块操作方法
1. OLED操作原理
要操作OLED,只需使用SPI接口发送数据,并不需要使用SPI接口读取数据, 除此之外,还需要控制D/C引脚:
- 当DC引脚是低电平时,是命令:比如复位、打开显示、设置地址
- 当DC引脚是高电平时,是数据:写入要显示的数据
2. 显存和像素
OLED上有128*64个像素,每个像素只有2种状态:亮、灭。
显存中,每位对应一个像素,入下图所示:
- byte0的8位数据对应屏幕上角左侧、竖向排列的8个像素,即COL0的像素,bit0对应第0行,bit1对应第1行,……
- byte1对应COL1那列第0~第7行的8个像素
- ……
- byte127对应COL127那列第0~第7行的8个像素
3. 显存寻址模式
显存被分为8页、128列,要写某个字节时,需要先指定地址,然后写入1字节的数据
- 哪页(Page)?
- 哪列(Col)?
- 写入1字节数据
OLED有三种寻址模式:
1> 页地址模式(Page addressing mode):每写入1个字节,行地址不变,列地址增1,列地址达到127后会从0开始
2> 水平地址模式(Horizontal addressing mode)
- 每写入1个字节,行地址不变,列地址增1
- 列地址达到127后从0开始,行地址指向下一页
- 列地址达到127、行地址达到7时,列地址和行地址都被复位为0,指向左上角
3> 垂直地址模式(Vertical addressing mode)
- 每写入1个字节,行地址增1,列地址不变
- 行地址达到7后从0开始,列地址指向下一列
- 列地址达到127、行地址达到7时,列地址和行地址都被复位为0,指向左上角
4. 初始化
5. 设置地址
- 设置地址模式
- 设置page
- 设置col
设置地址模式,比如设置为页地址模式时,先写命令0x22,再写命令0x02。页地址模式是默认的,可以不设置。
设置页地址:有0~7页,想设置哪一页(n)就发出命令:0xB0 | n。
设置列地址:列地址范围是0~127,需要使用2个命令来设置列地址。
6. 写入数据
让DC引脚为高,发起SPI写操作即可。
五. 自定义总线
-
创建并注册总线
//匹配方法,返回1表示匹配成功 int mybus_match(struct device *pdev, struct device_driver *pdrv) { struct test_dev * my_dev; struct test_drv * my_drv; my_dev = container_of(pdev, struct test_dev, dev); my_drv = container_of(pdrv, struct test_drv, driver); if(my_drv->number == my_dev->serial) return 1; return 0; } struct bus_type my_bus = { .name = "DaoGe", .match = mybus_match, }; bus_register(&my_bus);
-
测试总线
1> 创建类型 //类似platform_device, i2c_client, spi_device struct test_dev { int serial; struct device dev; }; //类似platform_driver, i2c_driver, spi_driver struct test_drv { int number; struct device_driver driver; int (*probe)(struct test_dev * pdev); int (*remove)(struct test_dev * pdev); }; 2> 创建对象 struct test_dev tdev = { .serial = 0x1234, .dev = { .init_name = "test_dev", .bus = &my_bus, .release = mybus_release, }, }; struct test_drv tdrv = { .number = 0x1234, .driver = { .name = "test_drv", .bus = &my_bus, .probe = mybus_parent_probe, .remove = mybus_parent_remove, }, .probe = mybus_probe, .remove = mybus_remove, }; 3> 注册 device_register(&tdev.dev); driver_register(&tdrv.driver); 4> 实现probe和remove int mybus_probe(struct test_dev * pdev) { printk("--------------%s--------------\n",__FUNCTION__); return 0; } int mybus_remove(struct test_dev * pdev) { printk("--------------%s--------------\n",__FUNCTION__); return 0; } int mybus_parent_probe(struct device *pdev) { struct test_dev * my_dev; struct test_drv * my_drv; struct device_driver *pdrv; pdrv = pdev->driver; my_dev = container_of(pdev, struct test_dev, dev); my_drv = container_of(pdrv, struct test_drv, driver); my_drv->probe(my_dev); return 0; } int mybus_parent_remove(struct device *pdev) { struct test_dev * my_dev; struct test_drv * my_drv; struct device_driver *pdrv; pdrv = pdev->driver; my_dev = container_of(pdev, struct test_dev, dev); my_drv = container_of(pdrv, struct test_drv, driver); my_drv->remove(my_dev); return 0; }
六.CUBEMX生成设备树
1、找到Pinout & Configuration左边的I2C1(例)
2、选择linux后在右边点击引脚配置
3、生成工程
4、路径:…\spi4\CA7\DeviceTree\spi4\kernel 下找dts文件
七.移位寄存器M74HC595级联spi驱动数码管(驱动层)
-
创建spi从机结构体指针,创建SPI驱动结构体,创建SPI总线,创建杂项驱动.自定义特殊宏
#define M74HC595_SET _IOW('S',0x00,int) //创建spi从机结构体指针 struct spi_device * m74hc595_device; //显示0-9的对应码值 char code[] = { 0x3f,/*display 0*/ 0x06,/*display 1*/ 0x5b,/*display 2*/ 0x4f,/*display 3*/ 0x66,/*display 4*/ 0x6d,/*display 5*/ 0x7d,/*display 6*/ 0x07,/*display 7*/ 0x7f,/*display 8*/ 0x6f /*display 9*/}; //杂项驱动的文件操作 const struct file_operations spi_m74hc595_fops = { .owner = THIS_MODULE, .unlocked_ioctl = spi_m74hc595_drv_ioctl, }; //注册杂项驱动 struct miscdevice misc = { .minor = 199, .name = "spi_m74hc595", .fops = &spi_m74hc595_fops, }; //创建SPI总线 const struct of_device_id spi_m74hc595_table[] = { { .compatible = "stm32mp157,spi_m74hc595"}, { }, }; //SPI驱动结构体 struct spi_driver spi_m74hc595_drv = { .probe = spi_m74hc595_drv_probe, .remove = spi_m74hc595_drv_remove, .driver = { .name = "stm32_spi_m74hc595", .of_match_table = spi_m74hc595_table, }, };
-
入口函数注册SPI设备
static int __init spi_m74hc595_drv_init(void) {//注册SPI设备 return spi_register_driver(&spi_m74hc595_drv); }
-
出口函数反操作
static void __exit spi_m74hc595_drv_exit(void) { spi_unregister_driver(&spi_m74hc595_drv); }
-
probe方法里注册杂项驱动和硬件初始化(设备树里写了就不用了)
int spi_m74hc595_drv_probe(struct spi_device *spi) {//注册杂项设备 int ret; printk("----------------%s-----------------\n",__FUNCTION__); m74hc595_device = spi; ret = misc_register(&misc); return 0; }
-
remove反操作
int spi_m74hc595_drv_remove(struct spi_device *spi) { printk("----------------%s-----------------\n",__FUNCTION__); misc_deregister(&misc); return 0; }
-
数码管显示方法
int spi_send_data(struct spi_device *spi, unsigned short data) {//通过& 0xff可获得低8位数据,通过左移8位可获得高8位数据 int error; struct spi_message message; //SPI的读写缓冲区对 struct spi_transfer xfer[1] = {0}; unsigned char tx[2] = {0}; tx[0] = data & 0xff; printk("tx0 = %d\n",tx[0]); tx[1] = code[data>>8]; printk("tx0 = %d\n",tx[1]); spi_message_init(&message); xfer[0].tx_buf = (const void*)&tx; xfer[0].len = sizeof(tx)/sizeof(tx[0]); spi_message_add_tail(&xfer[0], &message); error = spi_sync(spi,&message); if(unlikely(error)) { dev_err(&spi->dev, "SPI read error: %d\n", error); } return 0; }
-
应用层调用接口ioctl
long spi_m74hc595_drv_ioctl(struct file * filp, unsigned int cmd, unsigned long arg) { int ret; //用户传来的数据,分别是对应码值、片选。因为ioctl不可以传两个字节 //于是将两个字节合在一起用short类型传输 unsigned short data; switch(cmd) { case M74HC595_SET: ret = copy_from_user(&data, (void *)arg, sizeof(data)); if(ret > 0) { printk("Fail to copy to user\n"); return -EFAULT; } ret = spi_send_data(m74hc595_device, data); break; default: printk("unKnown cmd!\n"); break; } return 0; }
八.移位寄存器M74HC595级联spi驱动数码管(应用层)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#define M74HC595_SET _IOR('S',0x00,int)
int main(int argc,const char *argv[])
{
int fd;
int ret;
char pos = 0;
char digital;
unsigned short value;
fd = open("/dev/spi_m74hc595", O_RDWR);
if(fd < 0)
{
perror("open");
exit(1);
}
while(1)
{
for(digital = 0;digital < 10;digital ++)
{
value = (digital << 8 | (1 << pos));
ret = ioctl(fd, M74HC595_SET, (unsigned long)&value);
if(ret < 0)
{
perror("Fail to ioctl");
return -1;
}
sleep(1);
pos = (pos + 1) % 4;
}
}
close(fd);
return 0;
}
十五、PCI外围设备互联
一.PCI配置寄存器
1.1、每个PCI设备都有一个私有的至少256字节的地址空间,前64字节是标准的(每个PCI设备都有),后面的空间依赖设备来配置。
配置寄存器里包含了如下信息:
1.厂商id,设备id等
2.设备工作时需要的io地址和mem地址起始地址以及长度
3.设备的irq号等
1.2、配置寄存器的作用
1.linux内核启动时会从pci设备的配置寄存器里读取内存/IO起始地址以及irq,并把这些信息赋值给struct pci_dev的相应成员。
2.PCI驱动也会读写配置寄存器获得/保存设备相关的信息。
1.3.系统启动时,BIOS(X86)会为每个PCI设备分配内存、IO空间以及IRQ号,并写入相应PCI设备的配置寄存器里去。
二.取得设备的io或mem
此pci用到了几个BAR,应该从哪个bar里读取所选的io地址或内容地址?答:看数据手册
读取内存或io地址的函数:
//bar取值(0~5)
unsigned long pci_resource_start(struct pci_dev *dev, int bar);
unsigned long pci_resource_end(struct pci_dev *dev, int bar);
unsigned long pci_resource_len(struct pci_dev *dev, int bar);
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);
IORESOURCE_IO:io端口;IORESOURCE_MEN:内存
十六、字符设备
一. 模块
-
注意事项
确保编译驱动所依赖的内核和运行驱动程序的内核的版本要完全一致
驱动属于内核代码,不能使用应用空间的C库。 -
命令
insmod xxx.ko //加载驱动 rmmod xxx/rmmod xxx.ko //卸载驱动 lsmod //查看当前由多少模块被装载 modinfo xxx.ko //查看驱动程序的信息
-
Makefile解析
//定义变量 KERNEL_DIR=/home/farsight/farsight/linux-5.4.31 #CUR_DIR=`pwd` CUR_DIR=$(shell pwd) DRV_NAME=module_test all: make -C $(KERNEL_DIR) M=$(CUR_DIR) modules clean: make -C $(KERNEL_DIR) M=$(CUR_DIR) clean install: cp -raf *.ko /opt/rootfs/drv_module obj-m = $(DRV_NAME).o
-
打印级别(Android的logcat也是类似)
1. printk打印 #define KERN_EMERG KERN_SOH "0" /* system is unusable */ #define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */ #define KERN_CRIT KERN_SOH "2" /* critical conditions */ #define KERN_ERR KERN_SOH "3" /* error conditions */ #define KERN_WARNING KERN_SOH "4" /* warning conditions */ #define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */ #define KERN_INFO KERN_SOH "6" /* informational */ #define KERN_DEBUG KERN_SOH "7" /* debug-level messages */ 通过修改系统的打印级别,让printk不打印 [root@farsight /drv_module]# cat /proc/sys/kernel/printk 7 4 1 7 [root@farsight /drv_module]# echo "4 4 1 7" > /proc/sys/kernel/printk [root@farsight /drv_module]# cat /proc/sys/kernel/printk 4 4 1 7 [root@farsight /drv_module]# lsmod [root@farsight /drv_module]# insmod test_module.ko [root@farsight /drv_module]# 通过宏定义 #define PRINTK printk #define PRINTK(x...) 2.dev_err打印 dev_err(&pdev->dev, "Failed to get GPIO for led1\n"); #define pr_err(format, ...) fprintf (stderr, format, ## __VA_ARGS__) #define pr_debug(format, ...) fprintf (stderr, format, ## __VA_ARGS__) #define pr_debug(format, ...) do {} while (0) #define dev_err(dev, format, ...) fprintf (stderr, format, ## __VA_ARGS__) #define dev_warn(dev, format, ...) fprintf (stderr, format, ## __VA_ARGS__)
二. 简单字符设备驱动
-
注意事项
分类1:字符设备 块设备 网络设备
分类2:input子系统 i2C子系统 串口子系统 MTD子系统详细步骤如下:
① 看原理图确定引脚,确定引脚输出什么电平才能点亮/熄灭LED
② 看主芯片手册,确定寄存器操作方法:哪些寄存器?哪些位?地址是?
③ 编写驱动:先写框架,再写硬件操作的代码
注意:在芯片手册中确定的寄存器地址被称为物理地址,在Linux内核中无法直接使用。
需要使用内核提供的ioremap把物理地址映射为虚拟地址,使用虚拟地址。 -
硬件操作
1> 直接操作硬件 stm32mp157->rcc = ioremap(RCC, sizeof(rcc_t)); if(IS_ERR(stm32mp157->rcc)) { printk("ioremap error\n"); ret = PTR_ERR(stm32mp157->rcc); goto device_create_err3; } stm32mp157->rcc->PLL4CR|= (1<<0); while((stm32mp157->rcc->PLL4CR & (1<<1)) == 0); stm32mp157->rcc->MP_AHB4ENSETR |= (1 << 4); stm32mp157->gpioz->MODER &= ~((0x3 << 10)|(0x3 << 12)|(0x3 << 14)); stm32mp157->gpioz->MODER |= (0x1 << 10)|(0x1 << 12)|(0x1 << 14); //设置为输出模式 stm32mp157->gpioz->OTYPER &= ~((0x1 << 5)|(0x1 << 6)|(0x1 << 7));//推娩输出 stm32mp157->gpioz->OSPEEDR &= ~((0x3 << 10)|(0x3 << 12)|(0x3 << 14)); //低速 stm32mp157->gpioz->ODR &= ~((0x1 << 5)|(0x1 << 6)|(0x1 << 7)); stm32mp157->gpioe->MODER &= ~((0x3 << 16)|(0x3 << 20)); stm32mp157->gpioe->MODER |= (0x1 << 16)|(0x1 << 20); //设置为输出模式 stm32mp157->gpioe->OTYPER &= ~((0x1 << 8)|(0x1 << 10));//推娩输出 stm32mp157->gpioe->OSPEEDR &= ~((0x3 << 16)|(0x3 << 20)); //低速 stm32mp157->gpioe->ODR &= ~((0x1 << 8)|(0x1 << 10)); 2> 内核提供的函数 //写寄存器 #define writeb __raw_writeb #define writew(b,addr) __raw_writew(__cpu_to_le16(b),addr) #define writel(b,addr) __raw_writel(__cpu_to_le32(b),addr) //读寄存器 #define readb __raw_readb #define readw(addr) __le16_to_cpu(__raw_readw(addr)) #define readl(addr) __le32_to_cpu(__raw_readl(addr)) 3> GPIO操作(需要用到设备树)
-
ioctl命令合成的宏
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
-
字符设备驱动代码编写步骤
1> 定义模块的入口和出口,遵守GPL协议 module_init(led_drv_init); module_exit(led_drv_exit); MODULE_LICENSE("GPL"); 2> 自定义结构体对象,并且实例化 struct stm32mp157 { int major; struct class * clazz; struct device * dev; gpio_t * gpioz; gpio_t * gpioe; rcc_t *rcc; }; struct stm32mp157 * stm32mp157; stm32mp157 = kzalloc(sizeof(struct stm32mp157), GFP_KERNEL); 3> 申请或者注册设备号 register_chrdev(unsigned int major, const char * name, const struct file_operations * fops) 或者 ret = register_chrdev_region(stm32mp157->dev_no, 1, "led_drv"); ret = alloc_chrdev_region(&stm32mp157->dev_no, 0, 1, "led_drv"); cdev_alloc(); cdev_init(stm32mp157->c_dev, &fops); cdev_add(stm32mp157->c_dev, stm32mp157->dev_no, 1); 4> 创建类 stm32mp157->clazz = class_create(THIS_MODULE, "led_class"); 5> 创建设备节点 stm32mp157->dev = device_create(stm32mp157->clazz, NULL, stm32mp157->dev_no, NULL, "led_drv"); 6> 硬件初始化 //映射(非GPIO方法) stm32mp157->GPC0_CON = ioremap(0xE0200060, 8); 7> 在出口函数中实现以上函数对应的释放函数 8> 实现file_operations中的函数
三. Linux中的重要结构体关系
-
struct cdev结构体
struct cdev --- 描述一个字符设备对象信息(设备号 + file_operations), 任何一个字符设备都对应一个cdev对象 struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; };
-
struct inode结构体
struct inode --- 描述文件系统中某个文件的属性(文件的权限,类型,uid,gid, 修改时间) struct inode { umode_t i_mode; uid_t i_uid; gid_t i_gid; unsigned int i_flags; struct timespec i_atime; struct timespec i_mtime; struct timespec i_ctime; const struct file_operations *i_fop; struct cdev *i_cdev; };
-
struct file结构体
struct file --- 描述进程打开的文件信息:文件名,标志(可读可写),文件偏移 struct file { struct path f_path; const struct file_operations *f_op; void *private_data; unsigned int f_flags; fmode_t f_mode; }
-
结构体之间的关系
struct task_struct | /* open file information */ struct files_struct *files; | struct file __rcu * fd_array[NR_OPEN_DEFAULT]; | struct path f_path; | struct dentry *dentry; | struct inode *d_inode; | struct cdev *i_cdev; | dev_t dev; const struct file_operations *ops;
-
系统调用函数简析
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode) | ret = do_sys_open(AT_FDCWD, filename, flags, mode); | //从fd_array获取一个空闲的位置,返回下标 fd = get_unused_fd_flags(flags); //创建struct file结构体对象 struct file *f = do_filp_open(dfd, tmp, &op, lookup); //把创建好的struct file放到fd下标的空位里面去 fd_install(fd, f); SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) | //获取struct file结构体对象 file = fget_light(fd, &fput_needed); if (file->f_op->write) ret = file->f_op->write(file, buf, count, pos);
四. 杂项设备驱动
-
结构体
struct miscdevice { int minor; //次设备号 const char *name; //设备节点名称 const struct file_operations *fops; //操作函数 };
-
函数
struct miscdevice { int minor; //次设备号 const char *name; //设备节点名称 const struct file_operations *fops; //操作函数 };
十七、系统移植(未完)
一. 一般过程
编译内核和设备树:make -j4 uImage dtbs LOADADDR=0xC2000040
初始化内核:make distclean
cp -raf arch/arm/boot/uImage /tftpboot/
cp -raf arch/arm/boot/dts/stm32mp157a-fsmp1a-extended.dtb /tftpboot/
板子调试:
1.设备层位置:/sys/devices/platform/leds/leds/led0
2.修改对应设备树参数做调试:echo 1 > brightness(echo (参数) > (变量))
1、原理图和芯片手册(例如点个灯)
硬件资源:LED1 PE10 LED2 PF10 LED3 PE8
2、驱动
drivers/leds.leds-gpio.c
//裁剪或增加内核内容
make menuconfig
Device Drivers --->
[*] LED Support --->
<*> LED Class Support
<*> LED Support for GPIO connected LEDs
3、设备树的修改
参考文档: Documentation/devicetree/bindings/leds/leds-gpio.txt(会有设备树每个变量的解释和说明)
官方标准的设备树文件: arch/arm/boot/dts/stm32mp15xx-fsmp1x.dtsi
添加led节点
4、编译内核设备树
5、测试
1、原理图和芯片手册(例如响个蜂鸣器)
硬件资源:
2、驱动
drivers/leds.leds-gpio.c
//裁剪或增加内核内容
make menuconfig
Device Drivers --->
[*] Multifunction device drivers --->
<*> Support for STM32 Timers
[*] Pulse-width Modulation Support --->
<*> STMicroelectronics STM32 PWM
Input device support --->
-*- Generic input layer (needed for keyboard, mouse, ...)
[*] Miscellaneous devices --->
<*> PWM beeper support
3、设备树的修改
内核对应文档: Documentation/devicetree/bindings/pwm/pwm-stm32.txt
Documentation/devicetree/bindings/input/pwm-beeper.txt
主板上基础设备树:arch/arm/boot/dts/stm32mp151.dtsi(定义了各种控制器定时器的基础设备树内容)
官方标准的设备树文件: arch/arm/boot/dts/stm32mp15xx-fsmp1x.dtsi
添加led节点
4、编译内核设备树
5、测试
//拷贝测试函数
farsight@ubuntu:~/farsight/stm32mp157/day7/beep_test.c$ cp beeper_test /tftpboot/
//板子上找到测试函数
[root@fsmp1a root]# tftp -g -r beeper_test 192.168.40.194
//给权限
[root@fsmp1a root]# chmod 777 beeper_test
//执行对应设备树
[root@fsmp1a root]# ./beepertest /dev/input/event0
&sdmmc3 {
arm,primecell-periphid = <0x10153180>;
pinctrl-names = "default", "opendrain", "sleep";
pinctrl-0 = <&sdmmc3_b4_wifi_pins_a>;
pinctrl-1 = <&sdmmc3_b4_od_wifi_pins_a>;
pinctrl-2 = <&sdmmc3_b4_sleep_wifi_pins_a>;
non-removable;
st,neg-edge;
bus-width = <4>;
vmmc-supply = <&v3v3>;
mmc-pwrseq = <&wifi_pwrseq>;
#address-cells = <1>;
#size-cells = <0>;
keep-power-in-suspend;
status = "okay";
brcmf: bcrmf@1 {
reg = <1>;
compatible = "brcm,bcm4329-fmac";
};
};
十八、drm驱动模型(未完)
一. 应用编程
流程:1、打开DRM设备
2、查找适配的'crtc + encoder + connector'
3、为crtc创建扫描帧缓冲区
4、KMS模式设置
5、绘制图像
二. 重要结构体
2.1 获取显示资源drmModeGetResources
drmModeRes *res;
int drm_fd;
drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
res = drmModeGetResources(fd);
//drmModeRes 包含了上述各个模组的统计信息和指针
typedef struct _drmModeRes {
int count_fbs;
uint32_t *fbs;
int count_crtcs;
uint32_t *crtcs;
int count_connectors;
uint32_t *connectors;
int count_encoders;
uint32_t *encoders;
uint32_t min_width, max_width;
uint32_t min_height, max_height;
} drmModeRes, *drmModeResPtr;
//因此我们通过该接口,可以获取到当前系统的显示资源。
-----------------------------------------------------------------------------------------
2.2 获取连接器drmModeGetConnector
drmModeConnector *connector;
connector = drmModeGetConnector(drm_fd, res->connectors[0]);
//当然一个系统可能具有多个连接器(比如笔记本电脑一般同时支持LCD、HDMI两个屏幕),我们一般是循环遍历drmModeRes的所有connector,找到我们想要显示的设备。
typedef struct _drmModeConnector {
uint32_t connector_id;
uint32_t encoder_id; /*当前连接的编码器 */
uint32_t connector_type;
uint32_t connector_type_id;
drmModeConnection connection;
uint32_t mmWidth, mmHeight; /*宽和高(mm) */
drmModeSubPixel subpixel;
int count_modes;
drmModeModeInfoPtr modes;
int count_props;
uint32_t *props; /*属性ID列表*/
uint64_t *prop_values; /*属性值列表*/
int count_encoders;
uint32_t *encoders; /*编码器ID列表*/
} drmModeConnector, *drmModeConnectorPtr;
//drmModeConnection 是连接状态,比如HDMI设备的插拔检测。
//另外连接器有两个重要的信息,一个是对应的编码器Encoder,另一个是显示模式drmModeMode。
//count_encoders和 *encoder是其对应的编码器数目和指针。
//drmModeModeInfoPtr 是显示模式,包含分辨率(hdisplay,vdisplay),同步,和帧率(vrefresh)等信息:
typedef struct _drmModeModeInfo {
uint32_t clock;
uint16_t hdisplay, hsync_start, hsync_end, htotal, hskew;
uint16_t vdisplay, vsync_start, vsync_end, vtotal, vscan;
uint32_t vrefresh;
uint32_t flags;
uint32_t type;
char name[DRM_DISPLAY_MODE_LEN];
} drmModeModeInfo, *drmModeModeInfoPtr;
-----------------------------------------------------------------------------------------
2.3 drmModeGetEncoder从connector获取对应的Encoder
drmModeEncoder *encoder;
encoder = drmModeGetEncoder(drm_fd, conn->encoders[i]);
//不同的connector对应的Encoder可能存在差异(HDMI的编码和LCD肯定不一样)
typedef struct _drmModeEncoder {
uint32_t encoder_id;
uint32_t encoder_type;
uint32_t crtc_id;
uint32_t possible_crtcs;
uint32_t possible_clones;
} drmModeEncoder, *drmModeEncoderPtr;
-----------------------------------------------------------------------------------------
2.4 获取CRTC
//上述,取得drmModeEncoder 时,drmModeEncoder包含了possible_crtcs,这是当前Encoder可用的CRTC掩码值,所以需要根据这个值拿到drmModeRes中crtcs数组的index,以取得对应的CRTC。
for (j = 0; j < res->count_crtcs; j++) {
if (encoder->possible_crtcs & (1 << j)) {
crtc_id = res->crtcs[j];
if (crtc_id > 0) {
my_drm->crtc_id = crtc_id;
drmModeFreeEncoder(encoder);
return 0;
}
}
crtc_id = -1;
}
2.5.1 创建dumb buffer
//dumb buffer如下,后三个参数值是ioctl之后返回的。其中handle是后续操作dumb buffer需要的句柄。
struct drm_mode_create_dumb {
__u32 height;
__u32 width;
__u32 bpp;
__u32 flags;
/*上面3个参数是要发送的,要初始化*/
/* handle, pitch, size 3个参数是由驱动返回的*/
__u32 handle;
__u32 pitch;
__u64 size;
};
附录
一、配置busybox的语句
make defconfig
使用默认配置来配置一下 busybox
make menuconfig
busybox 也支持图形化配置
sudo make show-targets
输入如下命令查看当前 buildroot 所有配置了的目标软件包
sudo make busybox
单独编译并安装 busybox
sudo make
编译完成以后重新编译 buildroot,主要是对其进行打包。