一、目标
根据前面对汇编的介绍,我们来实现一个stm32f103的硬件I2C从MS4525压力传感器读数据的驱动。笔者的硬件是103的I2C2连到了MS4525上面。
需求描述
需求 | 描述 |
---|---|
C函数封装 | 要封装成C语言可以调用的函数,在其他的C函数中调用 |
传感器数值读取 | 该函数执行一次就完成一次从传感器处读压力值。即每次用传感器读2个字节 |
有关的问题
很多人说stm32f103的硬件i2c有bug,其实主要就是两个
Bug | 解决方案 |
---|---|
初始化时I2C的GPIO时钟使能不生效 | 如果你是用的cube配置的引脚,那就是用hal库初始化。去HAL_I2C_MspInit()函数中优化。参见笔者的另一篇文章:《STM32F103与4525I压力传感器通讯中的硬件I2C的解决方案》 |
SR2_busy位锁定位1 | 烧录一个不使用该I2C端口的程序,关机重启。修改你的代码再进行调试。只有硬件关机才能解锁。对调试不友好。 |
再有人说,总是调不通。这其实很有可能是用C语言不能严格执行这个端口要求的寄存器读取时序。
首先是使能CR1的PE,发送START。
置位CR1 - START位以后,轮询SR - SB位。如果置位了,则要读一次SR1再写入从机地址(即MS4525的地址),才能清SB位。
轮询SR1- ADDR位,如果该位置位,则需要通过读一次SR1再读一次SR2清除。之后,由于我们只是读2个字节,所以必须参考手册后面的文本。可以看到,如果只是读2个字节,要的操作是不一样的。这个非常重要。
所以说,如果读手册,一定要仔细点哈哈。
至于MS4525那边,其实比较简单,就是发送起始符,发送地址,读两个字节,发送NACK和终止符。就算是完成一次会话。
二、代码和解释
搞清楚了状况,我们就着手写程序。
第一步,创建presSen_asm.s文件。 注意,很多时候我们可能系统中有个presSen.c的文件,不可以直接创建presSen.s,因为这两个文件一旦编译都是生成presSen.o,会冲突。
第二步,设计程序。
函数定义笔者输出一个函数叫`uint16_t asm_Func_presSen_getVal(void);
那四句套话 把开篇那四句从startup_stm32f103xe.s里抄过来。解锁32位Thumb-2指令。
符号定义 考虑到程序中会用到I2C2有关的寄存器CR1、DR、SR1、SR2, 用到的位主要是CR1的START、POS、ACK、STOP和PE,SR1的SB、RXNE、ADDR、BTF。因为这些都是local的,所以可以直接使用该符号而没有必要提前用.local声明。我们给I2C2_BaseAddr赋值为这个口的寄存器的起始地址,剩下的相关寄存器只要定义个偏移量就可以了。如果你不记得这些寄存器的偏移量,去手册里这个位置查看。基地址也可以从手册里看,也可以从C的头文件里找。这样用LDR和STR就可以通过基地址+偏移量直接访问了。
那些.section .text.asm_Func_presSen_getVal后面的那些设置选项,参考一些关于汇编的伪指令
规划寄存器和内存 数数我们用多少存储。就像用C语言上来看看用几个变量。如果小于8个,就只要寄存器就可以了。本文就这么1个函数,来来回回就操作那么3个外设寄存器,再加上1个寄存器记录基地址,4个寄存器开局,就先不去开辟内存了,写了不够再去BSS段开辟就是了。
/*
* presSen_asm.s
*
* Created on: 2022年6月17日
* Author: SystemUser
*/
.syntax unified
.cpu cortex-m3
.fpu softvfp
.thumb
.global asm_Func_presSen_getVal
.set I2C2_BaseAddr, 0x40005800
.set CR1, 0x00
.set DR, 0x10
.set SR1, 0x14
.set SR2, 0x18
.set I2C_CR1_START, 0x100
.set I2C_CR1_STOP, 0x200
.set I2C_CR1_ACK, 0x400
.set I2C_CR1_POS, 0x800
.set I2C_CR1_PE, 0x01
.set I2C_CR1_POS_ACK, I2C_CR1_POS | I2C_CR1_ACK
.set I2C_SR1_ADDR, 0x02
.set I2C_SR1_SB, 0x01
.set I2C_SR1_BTF, 0x04
.set sensor_addr, 0x51
.section .text.asm_Func_presSen_getVal/*,"ax",%progbits*/
.type asm_Func_presSen_getVal,%function
/*
* Accroding to the manual, Case of two bytes to be received:
* – Set POS and ACK
* – Wait for the ADDR flag to be set
* – Clear ADDR
* – Clear ACK
* – Wait for BTF to be set
* – Program STOP
* – Read DR twice
*/
/*r3 is used to hold CR1, r4 for I2C2_BaseAddr, r5 for SR1, R6 for SR2*/
asm_Func_presSen_getVal:
push {r4-r7, lr}
/*for test*/
/*test over*/
ldr r4, =I2C2_BaseAddr; /* load the address of the I2C2 base register*/
ldr r3, [r4, #CR1];
Enable_the_I2C2:
orr r3, #I2C_CR1_PE /*Set the PE*/
str r3, [r4, CR1];
Send_a_Start:
orr r3, #I2C_CR1_START /*Send a start*/
str r3, [r4, CR1];
Check_if_the_i2c_start_isSent:
ldr r5, [r4, #SR1]; /*Keep reading the sr1 register*/
and r5, #I2C_SR1_SB
cmp r5, #I2C_SR1_SB
bne Check_if_the_i2c_start_isSent;
Send_the_Addr:
ldr r5, [r4, #SR1];
mov r3, #sensor_addr;
str r3, [r4, DR];
Set_POS_and_ACK:
ldr r3, [r4, #CR1];
orr r3, #I2C_CR1_POS_ACK /*Set the ACK and POS*/
str r3, [r4, CR1];
Wait_for_the_ADDR_flag_to_be_set:
ldr r5, [r4, #SR1];
and r5, #I2C_SR1_ADDR
cmp r5, #I2C_SR1_ADDR
bne Wait_for_the_ADDR_flag_to_be_set;
Clear_Addr:
ldr r5, [r4, #SR1];
ldr r6, [r4, #SR2];
Clear_ACK:
ldr r3, [r4, #CR1];
bic r3, I2C_CR1_ACK;
str r3, [r4, CR1];
Wait_for_BTF_to_be_set:
ldr r5, [r4, #SR1];
and r5, #I2C_SR1_BTF
cmp r5, #I2C_SR1_BTF
bne Wait_for_BTF_to_be_set;
Program_STOP:
ldr r3, [r4, #CR1];
orr r3, I2C_CR1_STOP;
str r3, [r4, CR1];
Read_DR_twice:
ldrb r2, [r4, #DR]
ldrb r1, [r4, #DR]
Form_the_return_value:
add r0, r1, r2, lsl 8;
Disable_I2C2:
mov r3, #0;
str r3, [r4, #CR1];
End_the_function:
pop {r4-r7, lr}
bx lr
.size asm_Func_presSen_getVal, .-asm_Func_presSen_getVal
定义函数并实现 用前文说的方法定义函数asm_Func_presSen_getVal()。进入函数以后先把r4 - r7压入栈里。因为这次个寄存器是被调用函数保护寄存器。剩下的每一段的语句块的含义都跟语句标号一样。最后由于传感器两次传来的值是两个8位的,分别存于r2和r1的低8位。用一句add r0, r1, r2, lsl 8;
将传感器的16位值算出并存入r0,也就是返回值。
告诉调试器函数尺寸 最后一句.size asm_Func_presSen_getVal, .-asm_Func_presSen_getVal
告诉调试器这个函数的大小。
三、运行情况
编译通过以后,从内存分布上检查一下。本函数占104个字节,除了4个压栈,没有额外的内存消耗。
测试线程的C代码如下所示。在tskTest.c里创建了一个测试线程,循环读取传感器的值。
/*
* tskTest.c
*/
#include "tskTest.h"
#include "FreeRTOS.h"
#include "task.h"
#include "stdbool.h"
#include "presSen.h"
static void init(void);
const TskTest_Def tskTest = {
.init = init,
};
#define STACK_SIZE 128
static StaticTask_t TCB_tskTest;
static StackType_t stack_tskTest[STACK_SIZE];
static TaskHandle_t tskTest_handle;
static void tskTest_Entry(void*);
void init(void){
tskTest_handle = xTaskCreateStatic(
tskTest_Entry,
"tskTest",
STACK_SIZE,
(void*)0,
4,
stack_tskTest,
&TCB_tskTest
);
}
void tskTest_Entry(void* p){
static __USED uint16_t sensorVal;
while(1){
sensorVal = presSen.get_SenVal(unit_cmH2O);
vTaskDelay(pdMS_TO_TICKS(500));
sensorVal = 0;
}
}
目前还是把跟这个I2C端口相关的函数封装到一个C结构体下。并且定义了
/*
* * presSen.h
*/
#ifndef PRESSEN_INC_PRESSEN_H_
#define PRESSEN_INC_PRESSEN_H_
#include "stdint.h"
typedef struct _PresSen_Def{
void (*init)(void);
uint16_t (*get_SenVal)(void);
}PresSen_Def;
extern const PresSen_Def presSen;
#endif /* PRESSEN_INC_PRESSEN_H_ */
在C的源程序文件中,暂时先保留init()函数的C语言实现,虽然为空函数。实例化presSen这个块。用extern关键字将汇编函数引进本头文件,并赋给presSen的get_val成员。
/*
* presSen.c
*
* Created on: 2022年6月11日
* Author: SystemUser
*/
#include "presSen.h"
#include "main.h"
#include "stdbool.h"
/* The address of the sensor is actually 0x28. But the address used in the
* program */
static void init(void);
/*Using get_SenVal() to call get_SenVal_unit() to make sure the structure preesSen
* is placed in the .text section by the linker.*/
extern uint16_t asm_Func_presSen_getVal(void);
const PresSen_Def presSen = {
.init = init,
.get_SenVal = asm_Func_presSen_getVal,
};
void init(void){
}
可以看出,每次运行sensorVal都能正确的获得传感器的读数。该驱动程序工作正常。
四、总结
可以看出,用汇编可以严格实现手册上规定的寄存器访问时序,节省内存开支,实现高效的驱动。