下游市场急降温,库存数据藏玄机,芯天下业绩“变脸”在即?
本文由【万点】原创
作者/青原
编辑/成宇
2022年4月,在烈火烹油的集成电路投融资赛道,又一家企业-芯天下技术股份有限公司(以下简称“”芯天下“”)向深交所递交创业板上市申请,这家有红杉中国、国投创业和深创投等知名机构投资的热门赛道厂商,在经历两轮问询后,将于11月18日创业板首发上会。
如果顺利过会,芯天下又将复制一场创始人、一级市场投资人、保荐机构皆大欢喜的财富盛筵,然而围绕这家公司的诸多不确定性,可能会给二级市场投资者带来不容忽视的风险。
01 满园深雪,一枝独秀?
招股书显示,芯天下是一家专业从事代码型闪存芯片研发、设计和销售的高新技术企业,提供从1Mbit-8Gbit宽容量范围的代码型闪存芯片,是业内代码型闪存芯片产品覆盖范围较全面的厂商之一。公司现有主要产品包括NOR Flash 和SLC NANDFlash,广泛应用于消费电子、网络通讯、物联网、工业与医疗等领域,所推出系列产品在兼容性、可靠性等方面在业界取得了良好的口碑和市场业绩,已获得英特尔、联发科、瑞昱、全志科技、瑞芯微、博通集成等多家主控厂商的认证,公司产品已进入三星、美的、科沃斯、爱都科技、中兴通讯、四川长虹、移远通信等知名品牌厂商的供应链体系并实现大批量交付和使用。
在招股书描绘的亮点之外,一个基本信息值得投资者特别留意: NORFlash 和SLC NANDFlash市场行情自今年二季度末以来正急转直下,快速降温,而且这样的波动绝非短期因素。专业市场分析机构IDC就预计,存储半导体市场的下行周期将从今年持续至2025年,另一研究机构YoleIntelligence也认为,存储芯片市场当前已供过于求,尽管厂商已经开始削减支出和产量,但其效应要在6-12个月后方能体现,尤其对于NANDFlash为代表的非易失存储器件,由于厂商在衰退期起点的利润率普遍更低,因此可能将出现大面积亏损,甚至不排除将出现厂商间的并购重组洗牌。
如此严峻的市场环境下,头部闪存厂商业绩也纷纷出现“变脸”,以NORFlash全球市占率第一的旺宏电子为例,该公司月度营收同比增速在今年7月份后就持续处于负值区间,并且同比下滑幅度逐月拉大(从七月份的-16%到十月份的-35%),第三季度营业收入总额同比下跌23%,当期净利润同比下跌54%。分产品看,当季旺宏电子NORFlash产品线营收环比下跌14%,同比下跌26%,按存储容量计算出货量跌至2019年三季度后最低水平,而旺宏电子同样经营的NANDFlash产品营收表现更为“惨烈”,当季营收环比下跌37%,同比下跌54%。
相比之下,根据芯天下公司对交易所问询回复透露的数据,今年1-9月该公司实现营收约为7.09亿元人民币,已接近去年全年水平,较去年同期则增长21.97%,扣除非经常性损益后归属于母公司股东的净利润较去年同期下降29.47%,公司还预计,2022年全年芯天下有望实现营收9.3至10亿元人民币,同比增速在15%以上。
从经营数据对比看,似乎市场行情对芯天下的影响远小于行业头部厂商,大客户的加持或许是一大原因。
芯天下方面透露,截至2022 年9 月21 日,公司在手订单约2.37 亿元,同比下降约37.72%,其中预计将于本年内交付的订单约1.57 亿元,同比增长约16.26%,主要原因为2021年公司产品开始大批量进入战略客户A客户的供应链,A客户在2021年下半年,根据下一年度对公司的采购计划进行大规模集中下单,使得2021 年下半年的在手订单处于较高水平。
虽然大客户订单集中交付,有助于芯天下抵御闪存市场“严寒”,但随着今年交付高峰过去,芯天下抵御市场周期的能力依然面临重大考验。
02 库存数据暗藏玄机
在招股书中,芯天下方面也的确提到了下游市场行情变化。
如招股书提及,2022年1-8月,受下游市场需求影响,公司各产品销售价格出现一定下降,同时由于采购价格变动有所滞后,单位成本仍维持在较高水平,导致2022年1-8月毛利率同比下降幅度较大。
而市场波动对存货的影响,也是交易所问询函和芯天下回复的一个重点。
深交所方面要求芯天下说明2022年计提跌价准备的原材料具体情况,是否具有通用性;结合库龄超过一年以上存货价格情况说明跌价准备计提是否充足;2022年全年预计资产减值损失金额,并在招股说明书重大风险提示内容中补充资产减值损失风险。
芯天下方面回复称,NOR产品及SLCNAND产品在2022年7月的销售单价均高于6月末的库龄12个月以上库存成本均价,公司计提存货跌价比例较高,存货跌价计提充足。
公司还表示,2022年1-6月,公司资产减值损失金额为-1543.53万元,均为存货跌价损失。结合下半年存货流转情况及期末的存货情况,公司初步预计测算全年资产减值损失金额约为-3000至-2700万元。
招股书还显示,2021年年末及2022年年中(6月末)公司存货跌价准备余额分别为1209.20万元和2216.60万元,占存货账面余额的比例分别为5.07%和8.26%,而2022年1-6月,公司资产减值损失金额为-1543.53万元,均为存货跌价损失。结合下半年存货流转情况及期末的存货情况,公司初步预计测算全年资产减值损失金额约为-3000至-2700万元,考虑存货跌价准备转销的影响后,预计2022年下半年因存货跌价导致净利润下降约750至1250万元。
然而细看芯天下存货数据,却可以发现其中或许暗藏玄机。
根据招股书公开信息,芯天下存货被分类为原材料、库存商品、委托加工物资和发出商品,除了处于货权转移过程中的发出商品,其他三类存货2022年上半年出现耐人寻味的变化:
原材料与库存商品账面余额较去年末微跌,计提的存货跌价准备却大幅提高;委托加工物资从去年末的5318.11万元,猛增至今年年中的7664.82万元,存货跌价准备却从236万元降至45万元。
何为委托加工物资?根据芯天下解释,委托加工物资包括送往晶圆测试厂进行检测的晶圆以及送往封装厂商进行封装测试的晶圆。
根据fabless模式流程,裸晶圆(die)完成封装测试后,才会最终成为可供交付客户的一颗颗芯片,而值得一提的是,这一环节也会成为一些IC设计公司调节业绩的“缓冲区”,在下游价格不利时,厂商会通过主动放慢在产品封装测试进度,将半成品晶圆“转移”到第三方封测厂商处,从而压低存货规模。
芯天下存货数据上的微妙变化究竟是何种原因?尚需更多信息以澄清。
03 委托研发、委托制造
众所周知,集成电路是一个具有极高技术含量的领域,各家厂商的市场地位,也相当程度上取决于其在技术上的核心竞争力。
而在技术研发上,芯天下却存在明显的短板,甚至公司招股书中也明确提示了相关风险,指出虽然已通过内部培养和外部引进等方式,组建了稳定的研发团队与管理团队,基本能够满足当前业务发展的需要。但随着公司业务规模的不断扩大及产品线的不断丰富,对具备扎实专业功底和丰富行业经验的高端人才的需求将日益增加。长期来看,公司目前的高端人才储备相对不足,需要加大高端技术人才的培养和引进力度,为未来持续、稳定的发展奠定人才基础。
芯天下方面目前有5名核心技术人员,分别为龙冬庆、苏志强、温靖康、王小光、唐维强,根据招股书中介绍的简历,其中包括包括龙冬庆在内的两名“核心技术人员”过往经历更多为FAE工程师乃至更为纯粹的销售人员,IC前端设计的核心人物温靖康于2013年 3月至 2017年 12月,就职于辉芒微电子(深圳)有限公司,任资深模拟设计工程师,2017年 12月至今在芯天下担任模拟IC 研发主管。
事实上,辉芒微也是芯天下NORFlash 技术的重要来源,2017年12月,芯天下以4000万元现金并发行9.0909%的股权,受让辉芒微SPI NOR Flash 产品线。
芯天下亦明确承认,公司与辉芒微交易过程中通过委托研发取得了与SPI NOR Flash产品相关的光罩、物理版图以及电路仿真及验证平台(检测电路功能及性能的辅助平台)和产品测试等方面的技术参考资料。
尽管芯天下方面表示,对于辉芒微技术资料,公司均通过自身研发活动进行了全面优化及升级,并且为实质性的优化,不过这样的技术发展路径,是否在未来能够直面头部厂商竞争,尚有待证明。
从NORFlash市场整体趋势看,传统通讯、PC、消费电子应用领域已成红海,汽车、工控、医疗、航空航天等领域应用则方兴未艾,但要打入这些高毛利市场,需要厂商有极强的正向研发能力。
目前,芯天下NOR Flash 覆盖1Mbit-1Gbit容量,产品主要应用于消费电子、网络通讯、物联网、工业与医疗等领域,相比华邦、旺宏等行业头部企业,产品容量范围仍存在一定劣势,对于车电子等市场应用覆盖较少,产品系列仍需进一步完善。
根据芯天下招股书,该公司核心产品除NORFlash之外,还有SLCNAND Flash,后者的存储晶圆直接采购自美日韩等知名IDM厂商,尽管负责ECC、坏块管理等功能的控制器晶圆有公司自行研发,形成了拥有自主知识产权的SPI NAND 控制器晶圆并已于2021 年实现量产。2021年及2022年 1-6月,包含公司自研控制器晶圆的SPI NAND(该产品包含1 颗自研控制器晶圆及1 颗存储晶圆)已实现19748.51 万元及19416.21万元的销售收入,但应该看到,由于作为组件功能和附加值核心的存储晶圆只能外购上游厂商成品,该产品线毛利率的提高面临挑战。
总体而言,芯天下所处的闪存芯片赛道,已经在近期出现了市场周期的急剧逆转,全行业面临严峻考验,这一形势,已经在行业头部厂商财报中有清晰体现,而各厂商在“寒冬”中的命运,相当程度上取决于其技术实力和供应链节奏调度。
在这样的大环境下,核心产品基础技术来自委托研发,制造封测同样依靠代工生态的芯天下,能否延续此前数年业绩高速增长的态势,成功穿越周期,为投资者创造价值?
对于这家颇具代表性的企业,万点研究将予以持续关注。
【驱动】SPI驱动分析(七)-SPI驱动常用调试方法
## 用户态
用户应用层使用spidev驱动的步骤如下:
1. 打开SPI设备文件:用户可以通过打开`/dev/spidevX.Y`文件来访问SPI设备,其中X是SPI控制器的编号,Y是SPI设备的编号。
2. 配置SPI参数:用户可以使用ioctl命令`SPI_IOC_WR_MODE`、`SPI_IOC_WR_BITS_PER_WORD`和`SPI_IOC_WR_MAX_SPEED_HZ`来设置SPI模式、数据位数和时钟速度等参数。
3. 发送和接收数据:用户可以使用read和write系统调用来发送和接收SPI数据。写入的数据将被传输到SPI设备,而从设备读取的数据将被存储在用户提供的缓冲区中。
4. 关闭SPI设备文件:当不再需要与SPI设备通信时,用户应该关闭SPI设备文件。
总结起来,spidev驱动提供了一种简单而灵活的方式来与SPI设备进行通信,使得用户可以轻松地在Linux系统上开发和控制SPI设备。
`spidev`驱动有现成的测试工具。其中一个常用的测试工具是`spi_test`,它是`spidev`驱动自带的测试工具,可以用于测试和调试SPI设备。`spi_test`可以通过命令行参数设置SPI设备的各种参数,如设备文件、传输速率、字节顺序等。使用spi_test可以发送和接收SPI数据,以验证spidev驱动的功能和性能。
在源码`Documentation\spi`路径下,有两个测试工具的源码文件,`spidev_fdx.c`和`spidev_test.c`文件。可以直接交叉编译为可执行文件使用。这些工具都基于`spidev`通用设备驱动以及对应的ioctl命令实现,可以方便的用来对spi的通用型驱动来进行测试。
`parse_opts`这段代码通过解析命令行选项,并根据选项的值设置相应的变量,实现了对命令行参数的解析和处理。
```c
static void parse_opts(int argc, char *argv[])
{
while (1) {
static const struct option lopts[] = {
{ "device", 1, 0, 'D' },
{ "speed", 1, 0, 's' },
{ "delay", 1, 0, 'd' },
{ "bpw", 1, 0, 'b' },
{ "loop", 0, 0, 'l' },
{ "cpha", 0, 0, 'H' },
{ "cpol", 0, 0, 'O' },
{ "lsb", 0, 0, 'L' },
{ "cs-high", 0, 0, 'C' },
{ "3wire", 0, 0, '3' },
{ "no-cs", 0, 0, 'N' },
{ "ready", 0, 0, 'R' },
{ "dual", 0, 0, '2' },
{ "verbose", 0, 0, 'v' },
{ "quad", 0, 0, '4' },
{ NULL, 0, 0, 0 },
};
int c;
c = getopt_long(argc, argv, "D:s:d:b:lHOLC3NR24p:v", lopts, NULL);
if (c == -1)
break;
switch (c) {
case 'D':
device = optarg;
break;
case 's':
speed = atoi(optarg);
break;
case 'd':
delay = atoi(optarg);
break;
case 'b':
bits = atoi(optarg);
break;
case 'l':
mode |= SPI_LOOP;
break;
case 'H':
mode |= SPI_CPHA;
break;
case 'O':
mode |= SPI_CPOL;
break;
case 'L':
mode |= SPI_LSB_FIRST;
break;
case 'C':
mode |= SPI_CS_HIGH;
break;
case '3':
mode |= SPI_3WIRE;
break;
case 'N':
mode |= SPI_NO_CS;
break;
case 'v':
verbose = 1;
break;
case 'R':
mode |= SPI_READY;
break;
case 'p':
input_tx = optarg;
break;
case '2':
mode |= SPI_TX_DUAL;
break;
case '4':
mode |= SPI_TX_QUAD;
break;
default:
print_usage(argv[0]);
break;
}
}
if (mode & SPI_LOOP) {
if (mode & SPI_TX_DUAL)
mode |= SPI_RX_DUAL;
if (mode & SPI_TX_QUAD)
mode |= SPI_RX_QUAD;
}
}
```
1. 声明一个静态的选项数组`lopts`,用于定义可接受的命令行选项。
2. 在循环内部,调用`getopt_long`函数来解析下一个选项。`getopt_long`函数会返回选项的短选项字符(c),如果没有更多选项则返回-1。
3. 使用switch语句根据选项的短选项字符进行分支处理。
4. 根据不同的选项,执行相应的操作。例如,对于选项'D',将其参数值赋给`device`变量;对于选项's',将其参数值转换为整数并赋给`speed`变量。
5. 如果遇到未知的选项,调用`print_usage`函数打印用法信息。
6. 循环结束后,根据设置的选项进行一些额外的逻辑处理。例如,如果设置了`SPI_LOOP`选项,则根据是否设置了`SPI_TX_DUAL`和`SPI_TX_QUAD`选项,设置相应的`SPI_RX_DUAL`和`SPI_RX_QUAD`选项。
`print_usage`打印spi_test的 使用方法。
```c
static void print_usage(const char *prog)
{
printf("Usage: %s [-DsbdlHOLC3]\n", prog);
puts(" -D --device device to use (default /dev/spidev1.1)\n"
" -s --speed max speed (Hz)\n"
" -d --delay delay (usec)\n"
" -b --bpw bits per word \n"
" -l --loop loopback\n"
" -H --cpha clock phase\n"
" -O --cpol clock polarity\n"
" -L --lsb least significant bit first\n"
" -C --cs-high chip select active high\n"
" -3 --3wire SI/SO signals shared\n"
" -v --verbose Verbose (show tx buffer)\n"
" -p Send data (e.g. \"1234\\xde\\xad\")\n"
" -N --no-cs no chip select\n"
" -R --ready slave pulls low to pause\n"
" -2 --dual dual transfer\n"
" -4 --quad quad transfer\n");
exit(1);
}
```
- `-D, --device <device>`:设置要使用的SPI设备,默认为`/dev/spidev1.0`。
- `-s, --speed <speed>`:设置SPI时钟速度,单位为Hz。
- `-d, --delay <delay>`:设置SPI传输之间的延迟时间,单位为微秒。
- `-b, --bits <bits>`:设置每个字的位数。
- `-l, --loop`:启用回环模式,将接收到的数据回送给发送方。
- `-H, --cpha`:将时钟相位设置为第二个边沿。
- `-O, --cpol`:将时钟极性设置为低电平活动。
- `-L, --lsb`:设置最低有效位(LSB)为先传输。
- `-C, --cs-high`:设置片选信号为高电平有效。
- `-3, --3wire`:设置3线SPI模式(共享SI/SO信号)。
- `-N, --no-cs`:禁用片选信号。
- `-v, --verbose`:启用详细输出模式,显示传输缓冲区的内容。
- `-t, --transfer <data>`:执行一个SPI传输,发送给定的数据字节。
- `-r, --read <count>`:执行一个SPI读传输,读取指定数量的字节。
- `-w, --write <data>`:执行一个SPI写传输,发送给定的数据字节。
- `-f, --file <file>`:从文件中读取数据并执行SPI传输。
- `-h, --help`:显示帮助信息。
`transfer`通过`ioctl`系统调用执行SPI数据传输操作。根据传入的参数和全局变量的设置,配置SPI传输的参数,并将发送和接收的数据进行打印。
```c
static void transfer(int fd, uint8_t const *tx, uint8_t const *rx, size_t len)
{
int ret;
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = len,
.delay_usecs = delay,
.speed_hz = speed,
.bits_per_word = bits,
};
if (mode & SPI_TX_QUAD)
tr.tx_nbits = 4;
else if (mode & SPI_TX_DUAL)
tr.tx_nbits = 2;
if (mode & SPI_RX_QUAD)
tr.rx_nbits = 4;
else if (mode & SPI_RX_DUAL)
tr.rx_nbits = 2;
if (!(mode & SPI_LOOP)) {
if (mode & (SPI_TX_QUAD | SPI_TX_DUAL))
tr.rx_buf = 0;
else if (mode & (SPI_RX_QUAD | SPI_RX_DUAL))
tr.tx_buf = 0;
}
ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
if (ret < 1)
pabort("can't send spi message");
if (verbose)
hex_dump(tx, len, 32, "TX");
hex_dump(rx, len, 32, "RX");
}
```
1.
2. 声明一个`spi_ioc_transfer`结构体变量`tr`,用于设置SPI传输的参数。
3. 在`spi_ioc_transfer`结构体中设置以下字段:
- `tx_buf`:指向发送数据缓冲区的指针。
- `rx_buf`:指向接收数据缓冲区的指针。
- `len`:要传输的数据长度。
- `delay_usecs`:传输之间的延迟时间(以微秒为单位)。
- `speed_hz`:SPI时钟速度(以赫兹为单位)。
- `bits_per_word`:每个字的位数。
4. 根据变量`mode`的值设置`tr`结构体中的`tx_nbits`和`rx_nbits`字段。如果`mode`中包含`SPI_TX_QUAD`标志,则将`tx_nbits`设置为4;如果`mode`中包含`SPI_TX_DUAL`标志,则将`tx_nbits`设置为2。类似地,如果`mode`中包含`SPI_RX_QUAD`标志,则将`rx_nbits`设置为4;如果`mode`中包含`SPI_RX_DUAL`标志,则将`rx_nbits`设置为2。
5. 如果`mode`中不包含`SPI_LOOP`标志,则根据`mode`中的其他标志设置`tr`结构体中的`tx_buf`和`rx_buf`字段。如果`mode`中包含`SPI_TX_QUAD`或`SPI_TX_DUAL`标志,则将`rx_buf`设置为0,表示在非回环模式下不接收数据。类似地,如果`mode`中包含`SPI_RX_QUAD`或`SPI_RX_DUAL`标志,则将`tx_buf`设置为0,表示在非回环模式下不发送数据。
6. 使用`ioctl`系统调用发送SPI消息并执行SPI数据传输操作。`SPI_IOC_MESSAGE(1)`表示发送单个SPI消息。
7. 检查`ioctl`的返回值`ret`,如果小于1,则表示SPI消息发送失败,调用`pabort`函数打印错误消息并终止程序。
8. 如果`verbose`标志为真,则使用`hex_dump`函数打印发送和接收数据的十六进制表示。
这段代码用于将输入字符串中的转义序列`\x`转换为对应的字符,并将结果存储在目标字符串中。它通过遍历输入字符串的字符,并根据转义序列的位置和格式进行解析和转换。
```c
static int unescape(char *_dst, char *_src, size_t len)
{
int ret = 0;
char *src = _src;
char *dst = _dst;
unsigned int ch;
while (*src) {
if (*src == '\\' && *(src+1) == 'x') {
sscanf(src + 2, "%2x", &ch);
src += 4;
*dst++ = (unsigned char)ch;
} else {
*dst++ = *src++;
}
ret++;
}
return ret;
}
```
`main`函数通过设置SPI设备的参数并执行数据传输操作与SPI设备进行通信。具体的数据传输操作在`transfer`函数中实现。
```c
int main(int argc, char *argv[])
{
int ret = 0;
int fd;
uint8_t *tx;
uint8_t *rx;
int size;
parse_opts(argc, argv);
fd = open(device, O_RDWR);
if (fd < 0)
pabort("can't open device");
/*
* spi mode
*/
ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
if (ret == -1)
pabort("can't set spi mode");
ret = ioctl(fd, SPI_IOC_RD_MODE32, &mode);
if (ret == -1)
pabort("can't get spi mode");
/*
* bits per word
*/
ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
if (ret == -1)
pabort("can't set bits per word");
ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
if (ret == -1)
pabort("can't get bits per word");
/*
* max speed hz
*/
ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
if (ret == -1)
pabort("can't set max speed hz");
ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
if (ret == -1)
pabort("can't get max speed hz");
printf("spi mode: 0x%x\n", mode);
printf("bits per word: %d\n", bits);
printf("max speed: %d Hz (%d KHz)\n", speed, speed/1000);
if (input_tx) {
size = strlen(input_tx+1);
tx = malloc(size);
rx = malloc(size);
size = unescape((char *)tx, input_tx, size);
transfer(fd, tx, rx, size);
free(rx);
free(tx);
} else {
transfer(fd, default_tx, default_rx, sizeof(default_tx));
}
close(fd);
return ret;
}
```
1. 调用`parse_opts`函数,解析命令行参数并设置全局变量。
2. 使用`open`函数打开SPI设备,以可读写方式打开。如果返回值小于0,则打印错误消息并终止程序。
3. 使用`ioctl`系统调用设置SPI设备的模式(`SPI_IOC_WR_MODE32`和`SPI_IOC_RD_MODE32`)、每字位数(`SPI_IOC_WR_BITS_PER_WORD`和`SPI_IOC_RD_BITS_PER_WORD`)以及最大时钟速度(`SPI_IOC_WR_MAX_SPEED_HZ`和`SPI_IOC_RD_MAX_SPEED_HZ`)。如果返回值为-1,则打印错误消息并终止程序。
4. 使用`printf`函数打印设置的SPI设备参数:模式、每字位数和最大时钟速度。
5. 如果`input_tx`不为NULL,则表示存在输入的发送数据。
- 计算输入发送数据的大小(排除末尾的`\0`)。
- 分配相应大小的内存给发送和接收缓冲区。
- 调用`unescape`函数,将输入发送数据中的转义序列反转义,并返回处理的字符数量。
- 调用`transfer`函数,执行SPI数据传输操作,将反转义后的发送数据发送到SPI设备,并接收数据到接收缓冲区。
- 释放发送和接收缓冲区的内存。
6. 否则,表示使用默认的发送和接收数据进行传输。
- 调用`transfer`函数,执行SPI数据传输操作,将默认的发送数据发送到SPI设备,并接收数据到接收缓冲区。
### spidev的缺点
使用read、write函数时,只能读、写,之二十半双工方式 使用ioctl可以达到全双工的读写 但是spidev有2个缺点:
- 不支持中断
- 只支持同步操作,不支持异步操作:就是read/write/ioctl这些函数只能执行完毕才可返回
完成代码如下
```c
/*
* SPI testing utility (using spidev driver)
*
* Copyright (c) 2007 MontaVista Software, Inc.
* Copyright (c) 2007 Anton Vorontsov <avorontsov@ru.mvista.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License.
*
* Cross-compile with cross-gcc -I/path/to/cross-kernel/include
*/
#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
static void pabort(const char *s)
{
perror(s);
abort();
}
static const char *device = "/dev/spidev1.1";
static uint32_t mode;
static uint8_t bits = 8;
static uint32_t speed = 500000;
static uint16_t delay;
static int verbose;
uint8_t default_tx[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x40, 0x00, 0x00, 0x00, 0x00, 0x95,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xF0, 0x0D,
};
uint8_t default_rx[ARRAY_SIZE(default_tx)] = {0, };
char *input_tx;
static void hex_dump(const void *src, size_t length, size_t line_size, char *prefix)
{
int i = 0;
const unsigned char *address = src;
const unsigned char *line = address;
unsigned char c;
printf("%s | ", prefix);
while (length-- > 0) {
printf("%02X ", *address++);
if (!(++i % line_size) || (length == 0 && i % line_size)) {
if (length == 0) {
while (i++ % line_size)
printf("__ ");
}
printf(" | "); /* right close */
while (line < address) {
c = *line++;
printf("%c", (c < 33 || c == 255) ? 0x2E : c);
}
printf("\n");
if (length > 0)
printf("%s | ", prefix);
}
}
}
/*
* Unescape - process hexadecimal escape character
* converts shell input "\x23" -> 0x23
*/
static int unescape(char *_dst, char *_src, size_t len)
{
int ret = 0;
char *src = _src;
char *dst = _dst;
unsigned int ch;
while (*src) {
if (*src == '\\' && *(src+1) == 'x') {
sscanf(src + 2, "%2x", &ch);
src += 4;
*dst++ = (unsigned char)ch;
} else {
*dst++ = *src++;
}
ret++;
}
return ret;
}
static void transfer(int fd, uint8_t const *tx, uint8_t const *rx, size_t len)
{
int ret;
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = len,
.delay_usecs = delay,
.speed_hz = speed,
.bits_per_word = bits,
};
if (mode & SPI_TX_QUAD)
tr.tx_nbits = 4;
else if (mode & SPI_TX_DUAL)
tr.tx_nbits = 2;
if (mode & SPI_RX_QUAD)
tr.rx_nbits = 4;
else if (mode & SPI_RX_DUAL)
tr.rx_nbits = 2;
if (!(mode & SPI_LOOP)) {
if (mode & (SPI_TX_QUAD | SPI_TX_DUAL))
tr.rx_buf = 0;
else if (mode & (SPI_RX_QUAD | SPI_RX_DUAL))
tr.tx_buf = 0;
}
ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
if (ret < 1)
pabort("can't send spi message");
if (verbose)
hex_dump(tx, len, 32, "TX");
hex_dump(rx, len, 32, "RX");
}
static void print_usage(const char *prog)
{
printf("Usage: %s [-DsbdlHOLC3]\n", prog);
puts(" -D --device device to use (default /dev/spidev1.1)\n"
" -s --speed max speed (Hz)\n"
" -d --delay delay (usec)\n"
" -b --bpw bits per word \n"
" -l --loop loopback\n"
" -H --cpha clock phase\n"
" -O --cpol clock polarity\n"
" -L --lsb least significant bit first\n"
" -C --cs-high chip select active high\n"
" -3 --3wire SI/SO signals shared\n"
" -v --verbose Verbose (show tx buffer)\n"
" -p Send data (e.g. \"1234\\xde\\xad\")\n"
" -N --no-cs no chip select\n"
" -R --ready slave pulls low to pause\n"
" -2 --dual dual transfer\n"
" -4 --quad quad transfer\n");
exit(1);
}
static void parse_opts(int argc, char *argv[])
{
while (1) {
static const struct option lopts[] = {
{ "device", 1, 0, 'D' },
{ "speed", 1, 0, 's' },
{ "delay", 1, 0, 'd' },
{ "bpw", 1, 0, 'b' },
{ "loop", 0, 0, 'l' },
{ "cpha", 0, 0, 'H' },
{ "cpol", 0, 0, 'O' },
{ "lsb", 0, 0, 'L' },
{ "cs-high", 0, 0, 'C' },
{ "3wire", 0, 0, '3' },
{ "no-cs", 0, 0, 'N' },
{ "ready", 0, 0, 'R' },
{ "dual", 0, 0, '2' },
{ "verbose", 0, 0, 'v' },
{ "quad", 0, 0, '4' },
{ NULL, 0, 0, 0 },
};
int c;
c = getopt_long(argc, argv, "D:s:d:b:lHOLC3NR24p:v", lopts, NULL);
if (c == -1)
break;
switch (c) {
case 'D':
device = optarg;
break;
case 's':
speed = atoi(optarg);
break;
case 'd':
delay = atoi(optarg);
break;
case 'b':
bits = atoi(optarg);
break;
case 'l':
mode |= SPI_LOOP;
break;
case 'H':
mode |= SPI_CPHA;
break;
case 'O':
mode |= SPI_CPOL;
break;
case 'L':
mode |= SPI_LSB_FIRST;
break;
case 'C':
mode |= SPI_CS_HIGH;
break;
case '3':
mode |= SPI_3WIRE;
break;
case 'N':
mode |= SPI_NO_CS;
break;
case 'v':
verbose = 1;
break;
case 'R':
mode |= SPI_READY;
break;
case 'p':
input_tx = optarg;
break;
case '2':
mode |= SPI_TX_DUAL;
break;
case '4':
mode |= SPI_TX_QUAD;
break;
default:
print_usage(argv[0]);
break;
}
}
if (mode & SPI_LOOP) {
if (mode & SPI_TX_DUAL)
mode |= SPI_RX_DUAL;
if (mode & SPI_TX_QUAD)
mode |= SPI_RX_QUAD;
}
}
int main(int argc, char *argv[])
{
int ret = 0;
int fd;
uint8_t *tx;
uint8_t *rx;
int size;
parse_opts(argc, argv);
fd = open(device, O_RDWR);
if (fd < 0)
pabort("can't open device");
/*
* spi mode
*/
ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
if (ret == -1)
pabort("can't set spi mode");
ret = ioctl(fd, SPI_IOC_RD_MODE32, &mode);
if (ret == -1)
pabort("can't get spi mode");
/*
* bits per word
*/
ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
if (ret == -1)
pabort("can't set bits per word");
ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
if (ret == -1)
pabort("can't get bits per word");
/*
* max speed hz
*/
ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
if (ret == -1)
pabort("can't set max speed hz");
ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
if (ret == -1)
pabort("can't get max speed hz");
printf("spi mode: 0x%x\n", mode);
printf("bits per word: %d\n", bits);
printf("max speed: %d Hz (%d KHz)\n", speed, speed/1000);
if (input_tx) {
size = strlen(input_tx+1);
tx = malloc(size);
rx = malloc(size);
size = unescape((char *)tx, input_tx, size);
transfer(fd, tx, rx, size);
free(rx);
free(tx);
} else {
transfer(fd, default_tx, default_rx, sizeof(default_tx));
}
close(fd);
return ret;
}
```
## 内核态
### DTS配置
```c
&spi0 {
status = "okay";
max-freq = <48000000>; //spi internal clk, don't modify
//dma-names = "tx", "rx"; //enable dma
pinctrl-names = "default"; //pinctrl according to you board
pinctrl-0 = <&spi0_clk &spi0_tx &spi0_rx &spi0_cs0 &spi0_cs1>;
spi_test@00 {
compatible = "rockchip,spi_test_bus0_cs0";
reg = <0>; //chip select 0:cs0 1:cs1
id = <0>;
spi-max-frequency = <24000000>; //spi output clock
//spi-cpha; not support
//spi-cpol; //if the property is here it is 1:clk is high, else 0:clk is low when idle
};
spi_test@01 {
compatible = "rockchip,spi_test_bus0_cs1";
reg = <1>;
id = <1>;
spi-max-frequency = <24000000>;
spi-cpha;
spi-cpol;
};
};
```
### 代码分析
```C
static int __init spi_rockchip_test_init(void)
{
int ret = 0;
misc_register(&spi_test_misc);
ret = spi_register_driver(&spi_rockchip_test_driver);
return ret;
}
module_init(spi_rockchip_test_init);
```
`spi_rockchip_test_init`函数,作为内核模块的初始化函数。在这个函数内部,执行以下操作:调用`misc_register`函数,将`spi_test_misc`结构体注册为一个misc设备。调用`spi_register_driver`函数,将`spi_rockchip_test_driver`结构体注册为一个SPI总线驱动程序。
```c
static struct spi_driver spi_rockchip_test_driver = {
.driver = {
.name = "spi_test",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(rockchip_spi_test_dt_match),
},
.probe = rockchip_spi_test_probe,
.remove = rockchip_spi_test_remove,
};
```
`spi_rockchip_test_driver`的SPI总线驱动程序结构体(`struct spi_driver`)。在这个结构体中,设置了以下成员变量:
- `.driver.name`:驱动程序的名称,设置为`"spi_test"`。
- `.driver.owner`:指向当前内核模块的指针,用于标识驱动程序的所有者。
- `.driver.of_match_table`:指向一个设备树匹配表的指针,用于与设备树中的设备进行匹配。
- `.probe`:指向`rockchip_spi_test_probe`函数的指针,表示当设备被探测到时,将调用该函数进行初始化。
- `.remove`:指向`rockchip_spi_test_remove`函数的指针,表示当设备被移除时,将调用该函数进行清理。
```c
static struct miscdevice spi_test_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "spi_misc_test",
.fops = &spi_test_fops,
};
```
定义了一个名为`spi_test_misc`的Misc设备结构体(`struct miscdevice`)。在这个结构体中,设置了以下成员变量:
- `.minor`:使用`MISC_DYNAMIC_MINOR`宏来动态分配一个未使用的次设备号。
- `.name`:设备的名称,设置为`"spi_misc_test"`。
- `.fops`:指向`spi_test_fops`的指针,将文件操作结构体与Misc设备关联起来。
```c
static const struct file_operations spi_test_fops = {
.write = spi_test_write,
};
```
首先,定义了一个名为`spi_test_fops`的文件操作结构体(`struct file_operations`)。在这个结构体中,只设置了其中的一个成员变量`.write`,将其指向了`spi_test_write`函数。这表明当文件被写入时,会调用`spi_test_write`函数来处理写操作。
```c
static int rockchip_spi_test_probe(struct spi_device *spi)
{
int ret;
int id = 0;
struct spi_test_data *spi_test_data = NULL;
if (!spi)
return -ENOMEM;
if (!spi->dev.of_node)
return -ENOMEM;
spi_test_data = (struct spi_test_data *)kzalloc(sizeof(struct spi_test_data), GFP_KERNEL);
if (!spi_test_data) {
dev_err(&spi->dev, "ERR: no memory for spi_test_data\n");
return -ENOMEM;
}
spi->bits_per_word = 8;
spi_test_data->spi = spi;
spi_test_data->dev = &spi->dev;
ret = spi_setup(spi);
if (ret < 0) {
dev_err(spi_test_data->dev, "ERR: fail to setup spi\n");
return -1;
}
if (of_property_read_u32(spi->dev.of_node, "id", &id)) {
dev_warn(&spi->dev, "fail to get id, default set 0\n");
id = 0;
}
g_spi_test_data[id] = spi_test_data;
printk("%s:name=%s,bus_num=%d,cs=%d,mode=%d,speed=%d\n", __func__, spi->modalias, spi->master->bus_num, spi->chip_select, spi->mode, spi->max_speed_hz);
return ret;
}
```
1. 首先,会做一个判空,传入的`spi`指针为空指针,表示没有有效的SPI设备,函数将返回错误码`ENOMEM`,表示内存不足。如果`spi`结构的`dev`成员中的`of_node`为空,表示设备没有有效的设备树节点,函数同样返回错误码`ENOMEM`。
2. 使用`kzalloc`分配了一块内存,大小为`struct spi_test_data`结构的大小。`kzalloc`是一个内核函数,它会将分配的内存区域清零。如果分配失败,将返回错误码`ENOMEM`。如果分配成功,将把指针赋给`spi_test_data`。如果分配失败,函数将打印错误信息,并返回错误码`ENOMEM`。
3. 将SPI设备的`bits_per_word`成员设置为8,表示每个字节使用8个位。
4. 将`spi`指针和`spi->dev`的地址分别赋给`spi_test_data`结构的成员变量`spi`和`dev`。
5. 调用`spi_setup`函数对SPI设备进行设置和初始化。如果返回值小于0,表示设置和初始化失败。函数将打印错误信息,并返回-1。
6. 这里使用`of_property_read_u32`函数从设备树节点中读取名为"id"的属性,并将其值存储在`id`变量中。如果读取失败,将打印警告信息,并将`id`设置为0。
7. 将`spi_test_data`指针存储在全局数组`g_spi_test_data`中的索引为`id`的位置。
8. 使用`printk`函数打印一条包含SPI设备的相关信息的调试信息。
```c
static ssize_t spi_test_write(struct file *file,
const char __user *buf, size_t n, loff_t *offset)
{
int argc = 0, i;
char tmp[64];
char *argv[16];
char *cmd, *data;
unsigned int id = 0, times = 0, size = 0;
unsigned long us = 0, bytes = 0;
char *txbuf = NULL, *rxbuf = NULL;
ktime_t start_time;
ktime_t end_time;
ktime_t cost_time;
memset(tmp, 0, sizeof(tmp));
if (copy_from_user(tmp, buf, n))
return -EFAULT;
cmd = tmp;
data = tmp;
while (data < (tmp + n)) {
data = strstr(data, " ");
if (!data)
break;
*data = 0;
argv[argc] = ++data;
argc++;
if (argc >= 16)
break;
}
tmp[n - 1] = 0;
if (!strcmp(cmd, "setspeed")) {
int id = 0, val;
struct spi_device *spi = NULL;
sscanf(argv[0], "%d", &id);
sscanf(argv[1], "%d", &val);
if (id >= MAX_SPI_DEV_NUM)
return n;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return n;
} else {
spi = g_spi_test_data[id]->spi;
}
spi->max_speed_hz = val;
} else if (!strcmp(cmd, "write")) {
char name[64];
int fd;
mm_segment_t old_fs = get_fs();
sscanf(argv[0], "%d", &id);
sscanf(argv[1], "%d", ×);
sscanf(argv[2], "%d", &size);
if (argc > 3) {
sscanf(argv[3], "%s", name);
set_fs(KERNEL_DS);
}
txbuf = kzalloc(size, GFP_KERNEL);
if (!txbuf) {
printk("spi write alloc buf size %d fail\n", size);
return n;
}
if (argc > 3) {
fd = sys_open(name, O_RDONLY, 0);
if (fd < 0) {
printk("open %s fail\n", name);
} else {
sys_read(fd, (char __user *)txbuf, size);
sys_close(fd);
}
set_fs(old_fs);
} else {
for (i = 0; i < size; i++)
txbuf[i] = i % 256;
}
start_time = ktime_get();
for (i = 0; i < times; i++)
spi_write_slt(id, txbuf, size);
end_time = ktime_get();
cost_time = ktime_sub(end_time, start_time);
us = ktime_to_us(cost_time);
bytes = size * times * 1;
bytes = bytes * 1000 / us;
printk("spi write %d*%d cost %ldus speed:%ldKB/S\n", size, times, us, bytes);
kfree(txbuf);
} else if (!strcmp(cmd, "read")) {
sscanf(argv[0], "%d", &id);
sscanf(argv[1], "%d", ×);
sscanf(argv[2], "%d", &size);
rxbuf = kzalloc(size, GFP_KERNEL);
if (!rxbuf) {
printk("spi read alloc buf size %d fail\n", size);
return n;
}
start_time = ktime_get();
for (i = 0; i < times; i++)
spi_read_slt(id, rxbuf, size);
end_time = ktime_get();
cost_time = ktime_sub(end_time, start_time);
us = ktime_to_us(cost_time);
bytes = size * times * 1;
bytes = bytes * 1000 / us;
printk("spi read %d*%d cost %ldus speed:%ldKB/S\n", size, times, us, bytes);
kfree(rxbuf);
} else if (!strcmp(cmd, "loop")) {
sscanf(argv[0], "%d", &id);
sscanf(argv[1], "%d", ×);
sscanf(argv[2], "%d", &size);
txbuf = kzalloc(size, GFP_KERNEL);
if (!txbuf) {
printk("spi tx alloc buf size %d fail\n", size);
return n;
}
rxbuf = kzalloc(size, GFP_KERNEL);
if (!rxbuf) {
kfree(txbuf);
printk("spi rx alloc buf size %d fail\n", size);
return n;
}
for (i = 0; i < size; i++)
txbuf[i] = i % 256;
start_time = ktime_get();
for (i = 0; i < times; i++)
spi_write_and_read_slt(id, txbuf, rxbuf, size);
end_time = ktime_get();
cost_time = ktime_sub(end_time, start_time);
us = ktime_to_us(cost_time);
if (memcmp(txbuf, rxbuf, size))
printk("spi loop test fail\n");
bytes = size * times;
bytes = bytes * 1000 / us;
printk("spi loop %d*%d cost %ldus speed:%ldKB/S\n", size, times, us, bytes);
kfree(txbuf);
kfree(rxbuf);
} else {
printk("echo id number size > /dev/spi_misc_test\n");
printk("echo write 0 10 255 > /dev/spi_misc_test\n");
printk("echo write 0 10 255 init.rc > /dev/spi_misc_test\n");
printk("echo read 0 10 255 > /dev/spi_misc_test\n");
printk("echo loop 0 10 255 > /dev/spi_misc_test\n");
printk("echo setspeed 0 1000000 > /dev/spi_misc_test\n");
}
return n;
}
```
1. 使用`memset`将`tmp`数组清零,然后使用`copy_from_user`从用户空间将数据拷贝到`tmp`数组中。如果拷贝失败,将返回错误码`EFAULT`。
2. 函数通过空格字符将命令和参数分隔开,并将它们存储在参数数组`argv`中。通过循环查找空格字符,并将空格替换为字符串结束符号,然后将下一个字符的地址存储在`argv`数组中。最后,将`tmp`数组的最后一个字符设置为字符串结束符号。
3. 据解析得到的命令,函数执行相应的操作。如果命令是"setspeed",则设置SPI设备的速度。如果命令是"write",则向SPI设备写入数据。如果命令是"read",则从SPI设备读取数据。如果命令是"loop",则进行SPI设备的循环测试。如果命令不匹配上述任何一个条件,则打印命令使用说明。
4. 当命令是"setspeed"时,代码会解析参数并设置指定的SPI设备的速度。
- 使用`sscanf`函数从参数数组`argv`中读取`id`和`val`的值,并将其存储在相应的变量中。
- 检查`id`是否超出最大SPI设备数量的限制。如果超出限制,函数将返回处理的字节数`n`。
- 检查对应的`g_spi_test_data[id]`是否为空,如果为空,则打印错误信息并返回处理的字节数`n`。
- 如果`g_spi_test_data[id]`不为空,将其对应的`spi`设备指针赋值给变量`spi`。
- 将`spi->max_speed_hz`设置为`val`,即设置SPI设备的速度。
5. 当命令是"write"时,代码会向指定的SPI设备写入数据。
- 使用`sscanf`函数从参数数组`argv`中读取`id`、`times`和`size`的值,并将其存储在相应的变量中。
- 如果参数个数大于3,说明还有一个文件名参数,使用`sscanf`函数从参数数组`argv`中读取文件名,并将其存储在`name`数组中。
- 如果参数个数大于3,说明有文件名参数,打开该文件并读取数据到`txbuf`中。
- 调用`ktime_get`函数获取当前时间作为测试开始时间。
- 通过循环调用`spi_write_slt`函数向SPI设备写入数据,循环次数为`times`次,每次写入的数据为`txbuf`,数据大小为`size`。
- 调用`ktime_get`函数获取当前时间作为测试结束时间,并计算测试所花费的时间。
- 通过计算总的数据量和测试时间,计算出传输速度,并打印相关信息。
6. 当命令是"read"时,代码会从指定的SPI设备读取数据。具体步骤与"write"命令类似,不同之处在于使用`spi_read_slt`函数从SPI设备读取数据,并计算读取的速度。
7. 当命令是"loop"时,代码将执行SPI设备的循环测试。
- 使用`sscanf`函数从参数数组`argv`中读取`id`、`times`和`size`的值,并将其存储在相应的变量中。
- 将循环测试的数据填充到`txbuf`数组中,每个字节的值为`i % 256`。
- 调用`ktime_get`函数获取当前时间作为测试开始时间。
- 通过循环调用`spi_write_and_read_slt`函数进行循环测试,循环次数为`times`次,每次向SPI设备写入`txbuf`数据,然后从SPI设备读取`size`字节的数据存储到`rxbuf`中。
- 调用`ktime_get`函数获取当前时间作为测试结束时间,并计算测试时间。
- 通过计算总的数据量和测试时间,计算出传输速度,并打印相关信息。
```c
int spi_write_and_read_slt(int id, const void *tx_buf,
void *rx_buf, size_t len)
{
int ret = -1;
struct spi_device *spi = NULL;
struct spi_transfer t = {
.tx_buf = tx_buf,
.rx_buf = rx_buf,
.len = len,
};
struct spi_message m;
if (id >= MAX_SPI_DEV_NUM)
return ret;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return ret;
} else {
spi = g_spi_test_data[id]->spi;
}
spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(spi, &m);
}
```
`spi_write_and_read_slt`通过SPI总线向指定的SPI设备进行同时写入和读取操作。它使用了`spi_transfer`结构体和`spi_message`结构体来描述数据传输的相关参数,并调用`spi_sync`函数执行SPI设备的同步传输操作,将`spi`和`m`作为参数传入。该函数会阻塞直到传输完成。。
```c
int spi_write_then_read_slt(int id, const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx)
{
int ret = -1;
struct spi_device *spi = NULL;
if (id >= MAX_SPI_DEV_NUM)
return ret;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return ret;
} else {
spi = g_spi_test_data[id]->spi;
}
ret = spi_write_then_read(spi, txbuf, n_tx, rxbuf, n_rx);
return ret;
}
```
这段代码通过SPI总线向指定的SPI设备进行先写后读的操作。它使用了`spi_write_then_read`函数来执行先写后读的操作,并将操作结果返回。
```c
int spi_read_slt(int id, void *rxbuf, size_t n)
{
int ret = -1;
struct spi_device *spi = NULL;
if (id >= MAX_SPI_DEV_NUM)
return ret;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return ret;
} else {
spi = g_spi_test_data[id]->spi;
}
ret = spi_read(spi, rxbuf, n);
return ret;
}
```
`spi_read_slt`通过SPI总线从指定的SPI设备进行读取操作。它使用了`spi_read`函数来执行读取操作,并将操作结果返回。
```c
int spi_write_slt(int id, const void *txbuf, size_t n)
{
int ret = -1;
struct spi_device *spi = NULL;
if (id >= MAX_SPI_DEV_NUM)
return -1;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return -1;
} else {
spi = g_spi_test_data[id]->spi;
}
ret = spi_write(spi, txbuf, n);
return ret;
}
```
spi_write_slt通过SPI总线向指定的SPI设备进行写入操作。它使用了`spi_write`函数来执行写入操作,并将操作结果返回。如果参数不合法或指定的SPI设备不存在,函数会直接返回-1。
### 测试命令
```c
echo write 0 10 255 > /dev/spi_misc_test
echo write 0 10 255 init.rc > /dev/spi_misc_test
echo read 0 10 255 > /dev/spi_misc_test
echo loop 0 10 255 > /dev/spi_misc_test
echo setspeed 0 1000000 > /dev/spi_misc_test
```
```bash
echo 类型 id 循环次数 传输长度 > /dev/spi_misc_test
echo setspeed id 频率(单位 Hz) > /dev/spi_misc_test
```
如果需要,可以自己修改测试 case。
### 常见问题
1. 调试前确认驱动有跑起来
2. 确保 SPI 4 个引脚的 IOMUX 配置无误
3. 确认 TX 送时,TX 引脚有正常的波形,CLK 有正常的 CLOCK 信号,CS 信号有拉低
4. 如果 clk 频率较高,可以考虑提高驱动强度来改善信号
### 完整代码
```C
/*drivers/spi/spi-rockchip-test.c -spi test driver
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
/* dts config
&spi0 {
status = "okay";
max-freq = <48000000>; //spi internal clk, don't modify
//dma-names = "tx", "rx"; //enable dma
pinctrl-names = "default"; //pinctrl according to you board
pinctrl-0 = <&spi0_clk &spi0_tx &spi0_rx &spi0_cs0 &spi0_cs1>;
spi_test@00 {
compatible = "rockchip,spi_test_bus0_cs0";
reg = <0>; //chip select 0:cs0 1:cs1
id = <0>;
spi-max-frequency = <24000000>; //spi output clock
//spi-cpha; not support
//spi-cpol; //if the property is here it is 1:clk is high, else 0:clk is low when idle
};
spi_test@01 {
compatible = "rockchip,spi_test_bus0_cs1";
reg = <1>;
id = <1>;
spi-max-frequency = <24000000>;
spi-cpha;
spi-cpol;
};
};
*/
/* how to test spi
* echo write 0 10 255 > /dev/spi_misc_test
* echo write 0 10 255 init.rc > /dev/spi_misc_test
* echo read 0 10 255 > /dev/spi_misc_test
* echo loop 0 10 255 > /dev/spi_misc_test
* echo setspeed 0 1000000 > /dev/spi_misc_test
*/
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/clk.h>
#include <linux/fs.h>
#include <linux/dma-mapping.h>
#include <linux/dmaengine.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/spi/spi.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/miscdevice.h>
#include <linux/hrtimer.h>
#include <linux/platform_data/spi-rockchip.h>
#include <asm/uaccess.h>
#include <linux/syscalls.h>
#define MAX_SPI_DEV_NUM 6
#define SPI_MAX_SPEED_HZ 12000000
struct spi_test_data {
struct device *dev;
struct spi_device *spi;
char *rx_buf;
int rx_len;
char *tx_buf;
int tx_len;
};
static struct spi_test_data *g_spi_test_data[MAX_SPI_DEV_NUM];
int spi_write_slt(int id, const void *txbuf, size_t n)
{
int ret = -1;
struct spi_device *spi = NULL;
if (id >= MAX_SPI_DEV_NUM)
return -1;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return -1;
} else {
spi = g_spi_test_data[id]->spi;
}
ret = spi_write(spi, txbuf, n);
return ret;
}
int spi_read_slt(int id, void *rxbuf, size_t n)
{
int ret = -1;
struct spi_device *spi = NULL;
if (id >= MAX_SPI_DEV_NUM)
return ret;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return ret;
} else {
spi = g_spi_test_data[id]->spi;
}
ret = spi_read(spi, rxbuf, n);
return ret;
}
int spi_write_then_read_slt(int id, const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx)
{
int ret = -1;
struct spi_device *spi = NULL;
if (id >= MAX_SPI_DEV_NUM)
return ret;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return ret;
} else {
spi = g_spi_test_data[id]->spi;
}
ret = spi_write_then_read(spi, txbuf, n_tx, rxbuf, n_rx);
return ret;
}
int spi_write_and_read_slt(int id, const void *tx_buf,
void *rx_buf, size_t len)
{
int ret = -1;
struct spi_device *spi = NULL;
struct spi_transfer t = {
.tx_buf = tx_buf,
.rx_buf = rx_buf,
.len = len,
};
struct spi_message m;
if (id >= MAX_SPI_DEV_NUM)
return ret;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return ret;
} else {
spi = g_spi_test_data[id]->spi;
}
spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(spi, &m);
}
static ssize_t spi_test_write(struct file *file,
const char __user *buf, size_t n, loff_t *offset)
{
int argc = 0, i;
char tmp[64];
char *argv[16];
char *cmd, *data;
unsigned int id = 0, times = 0, size = 0;
unsigned long us = 0, bytes = 0;
char *txbuf = NULL, *rxbuf = NULL;
ktime_t start_time;
ktime_t end_time;
ktime_t cost_time;
memset(tmp, 0, sizeof(tmp));
if (copy_from_user(tmp, buf, n))
return -EFAULT;
cmd = tmp;
data = tmp;
while (data < (tmp + n)) {
data = strstr(data, " ");
if (!data)
break;
*data = 0;
argv[argc] = ++data;
argc++;
if (argc >= 16)
break;
}
tmp[n - 1] = 0;
if (!strcmp(cmd, "setspeed")) {
int id = 0, val;
struct spi_device *spi = NULL;
sscanf(argv[0], "%d", &id);
sscanf(argv[1], "%d", &val);
if (id >= MAX_SPI_DEV_NUM)
return n;
if (!g_spi_test_data[id]) {
pr_err("g_spi.%d is NULL\n", id);
return n;
} else {
spi = g_spi_test_data[id]->spi;
}
spi->max_speed_hz = val;
} else if (!strcmp(cmd, "write")) {
char name[64];
int fd;
mm_segment_t old_fs = get_fs();
sscanf(argv[0], "%d", &id);
sscanf(argv[1], "%d", ×);
sscanf(argv[2], "%d", &size);
if (argc > 3) {
sscanf(argv[3], "%s", name);
set_fs(KERNEL_DS);
}
txbuf = kzalloc(size, GFP_KERNEL);
if (!txbuf) {
printk("spi write alloc buf size %d fail\n", size);
return n;
}
if (argc > 3) {
fd = sys_open(name, O_RDONLY, 0);
if (fd < 0) {
printk("open %s fail\n", name);
} else {
sys_read(fd, (char __user *)txbuf, size);
sys_close(fd);
}
set_fs(old_fs);
} else {
for (i = 0; i < size; i++)
txbuf[i] = i % 256;
}
start_time = ktime_get();
for (i = 0; i < times; i++)
spi_write_slt(id, txbuf, size);
end_time = ktime_get();
cost_time = ktime_sub(end_time, start_time);
us = ktime_to_us(cost_time);
bytes = size * times * 1;
bytes = bytes * 1000 / us;
printk("spi write %d*%d cost %ldus speed:%ldKB/S\n", size, times, us, bytes);
kfree(txbuf);
} else if (!strcmp(cmd, "read")) {
sscanf(argv[0], "%d", &id);
sscanf(argv[1], "%d", ×);
sscanf(argv[2], "%d", &size);
rxbuf = kzalloc(size, GFP_KERNEL);
if (!rxbuf) {
printk("spi read alloc buf size %d fail\n", size);
return n;
}
start_time = ktime_get();
for (i = 0; i < times; i++)
spi_read_slt(id, rxbuf, size);
end_time = ktime_get();
cost_time = ktime_sub(end_time, start_time);
us = ktime_to_us(cost_time);
bytes = size * times * 1;
bytes = bytes * 1000 / us;
printk("spi read %d*%d cost %ldus speed:%ldKB/S\n", size, times, us, bytes);
kfree(rxbuf);
} else if (!strcmp(cmd, "loop")) {
sscanf(argv[0], "%d", &id);
sscanf(argv[1], "%d", ×);
sscanf(argv[2], "%d", &size);
txbuf = kzalloc(size, GFP_KERNEL);
if (!txbuf) {
printk("spi tx alloc buf size %d fail\n", size);
return n;
}
rxbuf = kzalloc(size, GFP_KERNEL);
if (!rxbuf) {
kfree(txbuf);
printk("spi rx alloc buf size %d fail\n", size);
return n;
}
for (i = 0; i < size; i++)
txbuf[i] = i % 256;
start_time = ktime_get();
for (i = 0; i < times; i++)
spi_write_and_read_slt(id, txbuf, rxbuf, size);
end_time = ktime_get();
cost_time = ktime_sub(end_time, start_time);
us = ktime_to_us(cost_time);
if (memcmp(txbuf, rxbuf, size))
printk("spi loop test fail\n");
bytes = size * times;
bytes = bytes * 1000 / us;
printk("spi loop %d*%d cost %ldus speed:%ldKB/S\n", size, times, us, bytes);
kfree(txbuf);
kfree(rxbuf);
} else {
printk("echo id number size > /dev/spi_misc_test\n");
printk("echo write 0 10 255 > /dev/spi_misc_test\n");
printk("echo write 0 10 255 init.rc > /dev/spi_misc_test\n");
printk("echo read 0 10 255 > /dev/spi_misc_test\n");
printk("echo loop 0 10 255 > /dev/spi_misc_test\n");
printk("echo setspeed 0 1000000 > /dev/spi_misc_test\n");
}
return n;
}
static const struct file_operations spi_test_fops = {
.write = spi_test_write,
};
static struct miscdevice spi_test_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "spi_misc_test",
.fops = &spi_test_fops,
};
static int rockchip_spi_test_probe(struct spi_device *spi)
{
int ret;
int id = 0;
struct spi_test_data *spi_test_data = NULL;
if (!spi)
return -ENOMEM;
if (!spi->dev.of_node)
return -ENOMEM;
spi_test_data = (struct spi_test_data *)kzalloc(sizeof(struct spi_test_data), GFP_KERNEL);
if (!spi_test_data) {
dev_err(&spi->dev, "ERR: no memory for spi_test_data\n");
return -ENOMEM;
}
spi->bits_per_word = 8;
spi_test_data->spi = spi;
spi_test_data->dev = &spi->dev;
ret = spi_setup(spi);
if (ret < 0) {
dev_err(spi_test_data->dev, "ERR: fail to setup spi\n");
return -1;
}
if (of_property_read_u32(spi->dev.of_node, "id", &id)) {
dev_warn(&spi->dev, "fail to get id, default set 0\n");
id = 0;
}
g_spi_test_data[id] = spi_test_data;
printk("%s:name=%s,bus_num=%d,cs=%d,mode=%d,speed=%d\n", __func__, spi->modalias, spi->master->bus_num, spi->chip_select, spi->mode, spi->max_speed_hz);
return ret;
}
static int rockchip_spi_test_remove(struct spi_device *spi)
{
printk("%s\n", __func__);
return 0;
}
#ifdef CONFIG_OF
static const struct of_device_id rockchip_spi_test_dt_match[] = {
{ .compatible = "rockchip,spi_test_bus0_cs0", },
{ .compatible = "rockchip,spi_test_bus0_cs1", },
{ .compatible = "rockchip,spi_test_bus1_cs0", },
{ .compatible = "rockchip,spi_test_bus1_cs1", },
{ .compatible = "rockchip,spi_test_bus2_cs0", },
{ .compatible = "rockchip,spi_test_bus2_cs1", },
{ .compatible = "rockchip,spi_test_bus3_cs0", },
{ .compatible = "rockchip,spi_test_bus3_cs1", },
{ .compatible = "rockchip,spi_test_bus4_cs0", },
{ .compatible = "rockchip,spi_test_bus4_cs1", },
{},
};
MODULE_DEVICE_TABLE(of, rockchip_spi_test_dt_match);
#endif /* CONFIG_OF */
static struct spi_driver spi_rockchip_test_driver = {
.driver = {
.name = "spi_test",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(rockchip_spi_test_dt_match),
},
.probe = rockchip_spi_test_probe,
.remove = rockchip_spi_test_remove,
};
static int __init spi_rockchip_test_init(void)
{
int ret = 0;
misc_register(&spi_test_misc);
ret = spi_register_driver(&spi_rockchip_test_driver);
return ret;
}
module_init(spi_rockchip_test_init);
static void __exit spi_rockchip_test_exit(void)
{
misc_deregister(&spi_test_misc);
return spi_unregister_driver(&spi_rockchip_test_driver);
}
module_exit(spi_rockchip_test_exit);
MODULE_AUTHOR("Luo Wei <lw@rock-chips.com>");
MODULE_AUTHOR("Huibin Hong <hhb@rock-chips.com>");
MODULE_DESCRIPTION("ROCKCHIP SPI TEST Driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("spi:spi_test");
```
相关问答
spi 是什么意思的缩写[回答]SPI是串行外设接口(SerialPeripheralInterface)的缩写。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的...
spi 速率指什么?SPI作为master或slave时,可以达到的最大传输速率是:I2C协议v2.1规定了100K,400K和3.4M三种速率(bps)。SPI是一种事实标准,由Motorola开发,并没有一个官方标...
SPI 、I2C、USART等协议有什么共同点和区别?这个,还是先说说三种协议的特点吧。1、SPI。串行通信协议,全双工的同步通信总线协议,要分主从,要占四根线(四个管脚),算是速度比较高的一种了(这三种...它通讯...
IIC和 SPI 有什么区别?它采用双线制,即SDA和SCL线,可以传输多个设备的数据,但是速度较慢,最高只能达到400Kbps。而SPI是另一种串行通信接口,通常用于连接微控制器和各种外设,如显...
怎么写linuxspimast与device如何匹配?内核版本2.6.30。编进内核的SPI驱动,通过看代码我明白了,大致过程是这样:1、先创建一个spi_board_info结构描述spi设备信息,调用spi_register_board_info将...
qspi协议?QSPI是QueuedSPI的简写,是Motorola公司推出的SPI接口的扩展,比SPI应用更加广泛。在SPI协议的基础上,Motorola公司对其功能进行了增强,增加了队列传输机制,...
spi 和i2c有什么区别?1、定义不同SPI:SPI是串行外设接口(SerialPeripheralInterface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯...
Exynos4412包含几个 spi 接口?Exynos4412包含3个SPI接口。这些SPI接口分别位于GPIO控制器中,并且可以支持传输速度高达25MHz。这些接口可以用于连接外部设备,如传感器、存储器和其他外围设...
25d80是什么芯片?25d80是一款SPI接口的8Mbit闪存芯片,由Winbond生产。它能在3.3V或5V供电下工作,存储容量为1MB,具有快速读写速度和低功耗特性。该芯片广泛应用于嵌入式系统、...
物联网如何推动万物互联?作为消费科技巨头的苹果,在2014年就开始向智能家居延伸,推出HomeKit智能家居平台,通过接入第三方智能设备,利用iPhone等智能终端可以实现对家庭场景下各设备...