前言
首先这是一篇涉及内核分析的,学习这篇文章最好是打开内核源码跟着我的分析去看,我参考的内核源码是linux5.4内核,也可以辅助ai去分析。
Modbus RTU读取rs485温湿度传感器
使用Modbus RTU读取rs485温湿度传感器有俩种方法,
第一种采用gpio控制数据的传输方向:高电平表示主发从收,低电平表示主收从发。
第二种采用硬件流控的方法使用串口的rts引脚和cts引脚自动控制收发方向,接下来将会具体分析二者的区别。
经过验证使用硬件流控的方法去读取rs485温湿度传感器存在无法控制转换电平的时序问题无法正确接收,硬件流控是为了全双工通信而设计的,可以控制流量的一种方式,它不需要去设置gpio引脚,可以通过sport->port.mctrl |= TIOCM_RTS;设置控制寄存器直接由硬件控制硬件,这就是问题所在,它这个0,1代表的是是否可以发送和接受数据,切换的时候无延时,导致时序不对。这是我测量出来的,因此这里推荐使用rts-gpio去读取rs485温湿度传感器。如果有说的不对的地方,或者通过硬件流控的方式实现了单工读取rs485温湿度传感器可以私信我一起讨论。
采用gpio控制数据的传输方向
设备树
&uart2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart2>;
rts-gpios = <&gpio5 0 GPIO_ACTIVE_HIGH>; // GPIO5_0 作为 RTS
//5-rx-during-tx; //允许在发送数据的同时接收数据。全双工通信标志,单工的话可以不用设置
rs485-rts-delay = <100 100>; // 发送前后延时 100ms
rs485-rts-active-high; // 高电平有效
linux,rs485-enabled-at-boot-time; // 使能串口的rs485功能
status = "okay";
};
pinctrl_uart2: uart2grp {
fsl,pins = <
MX6UL_PAD_UART2_TX_DATA__UART2_DCE_TX 0x1b0b1
MX6UL_PAD_UART2_RX_DATA__UART2_DCE_RX 0x1b0b1
MX6UL_PAD_SNVS_TAMPER0__GPIO5_IO00 0x1b0b1
>;
};
串口485功能的内部实现机制:
分析主要结构体:
//在struct uart_port里面有struct serial_rs485 rs485成员。
struct serial_rs485 {
__u32 flags; /* RS485 feature flags */
#define SER_RS485_ENABLED (1 << 0) /* If enabled */
#define SER_RS485_RTS_ON_SEND (1 << 1) /* Logical level for
RTS pin when
sending */
#define SER_RS485_RTS_AFTER_SEND (1 << 2) /* Logical level for
RTS pin after sent*/
#define SER_RS485_RX_DURING_TX (1 << 4)
#define SER_RS485_TERMINATE_BUS (1 << 5) /* Enable bus
termination
(if supported) */
__u32 delay_rts_before_send; /* Delay before send (milliseconds) */
__u32 delay_rts_after_send; /* Delay after send (milliseconds) */
__u32 padding[5]; /* Memory is cheap, new structs
are a royal PITA .. */
};
flags:
用于启用或配置 485 功能的标志位
SER_RS485_ENABLED (1 << 0 ) | 启用 485 功能,如果未设置此标志,485 功能将被禁用 |
---|---|
SER_RS485_RTS_ON_SEND (`1 << 1) | 控制 RTS 引脚在发送数据时的电平。 如果设置,发送数据时 RTS 引脚为高电平(通常用于使能 485 发送器)。 |
SER_RS485_RTS_AFTER_SEND (1 << 2 ) | 控制 RTS 引脚在发送完成后的电平如果设置,发送完成后 RTS 引脚为低电平(通常用于禁用 485 发送器,使设备进入接收模式)。 |
SER_RS485_RX_DURING_TX (1 << 4 ) | 允许在发送数据的同时接收数据,某些 485 芯片支持全双工通信,此标志用于启用该功能。 |
SER_RS485_TERMINATE_BUS (1 << 5 ) | 启用总线终端电阻。 如果硬件支持,此标志用于启用总线终端电阻,以提高通信稳定性。 |
注:如果SER_RS485_RTS_ON_SEND未被设置则无法在发送时自动拉高引脚电平将导致发送失 败,通常和SER_RS485_RTS_AFTER_SEND`配合使用但是二者不能同时出现后面代码可以体现出来
delay_rts_before_send:
在发送数据前,RTS 引脚电平变化的延时(以毫秒为单位)。delay_rts_after_send:
在发送数据后,RTS 引脚电平变化的延时(以毫秒为单位)。padding[5]
:填充字段,用于未来扩展。
内核如何将设备树和这些信息绑定的呢?
串口的probe函数被调用时
static int imx_uart_probe_dt(struct imx_port *sport,
struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
int ret;
sport->devdata = of_device_get_match_data(&pdev->dev);
if (!sport->devdata)
/* no device tree device */
return 1;
ret = of_alias_get_id(np, "serial");
if (ret < 0) {
dev_err(&pdev->dev, "failed to get alias id, errno %d\n", ret);
return ret;
}
sport->port.line = ret;
//判断设备树启用的是gpio还是硬件流控的方式,来设置imx_port结构体里面的have_rtscts和have_rtsgpio成员。
if (of_get_property(np, "uart-has-rtscts", NULL) ||
of_get_property(np, "fsl,uart-has-rtscts", NULL) /* deprecated */)
sport->have_rtscts = 1;
if (of_get_property(np, "fsl,dte-mode", NULL))
sport->dte_mode = 1;
if (of_get_property(np, "rts-gpios", NULL))
sport->have_rtsgpio = 1;
return 0;
}
在/drivers/tty/serial/omap-serial.c文件的serial_omap_probe_rs485函数中
static int serial_omap_probe_rs485(struct uart_omap_port *up,
struct device_node *np)
{
struct serial_rs485 *rs485conf = &up->port.rs485;
int ret;
rs485conf->flags = 0;
up->rts_gpio = -EINVAL;
if (!np)
return 0;
uart_get_rs485_mode(up->dev, rs485conf);//这个函数获取到了设备树的属性写入rs485conf->flags中
if (of_property_read_bool(np, "rs485-rts-active-high")) {
rs485conf->flags |= SER_RS485_RTS_ON_SEND;
rs485conf->flags &= ~SER_RS485_RTS_AFTER_SEND;
} else {
rs485conf->flags &= ~SER_RS485_RTS_ON_SEND;
rs485conf->flags |= SER_RS485_RTS_AFTER_SEND;
}
/* check for tx enable gpio */
up->rts_gpio = of_get_named_gpio(np, "rts-gpio", 0);//获取到gpio引脚号并将其存储在up结构体的rts_gpio成员中。
if (gpio_is_valid(up->rts_gpio)) { //检查gpio编号是否有效有效返回非零值
ret = devm_gpio_request(up->dev, up->rts_gpio, "omap-serial");//请求该GPIO,并将其标记为“omap-serial”用途。将申请的gpio与设备绑定
if (ret < 0)
return ret;
ret = rs485conf->flags & SER_RS485_RTS_AFTER_SEND ? 1 : 0; //判断引脚是高电平有效还是低电平有效
ret = gpio_direction_output(up->rts_gpio, ret);
if (ret < 0)
return ret;
} else if (up->rts_gpio == -EPROBE_DEFER) {
return -EPROBE_DEFER;
} else {
up->rts_gpio = -EINVAL;
}
return 0;
}
注对于这个函数我想展开说说devm_gpio_request(up->dev, up->rts_gpio, "omap-serial");这涉及到linux内核中的资源管理,我们需要明白这个函数干了那些事
-
申请资源(例如 GPIO、内存等)。
-
将资源包装为一个
struct devres
结构体。 -
将
struct devres
添加到devres_head
链表中。
也就是说对于这些资源,内核处理的方法是将资源打包为一个struct devres 结构体,然后将它添加到设备管理的devres_head
链表中,这样设备在释放结构体的时候就会从这个列表中找到对应的释放函数。
uart_get_rs485_mode函数
void uart_get_rs485_mode(struct device *dev, struct serial_rs485 *rs485conf)
{
u32 rs485_delay[2];
int ret;
ret = device_property_read_u32_array(dev, "rs485-rts-delay",
rs485_delay, 2);
if (!ret) {
rs485conf->delay_rts_before_send = rs485_delay[0];
rs485conf->delay_rts_after_send = rs485_delay[1];
} else {
rs485conf->delay_rts_before_send = 0;
rs485conf->delay_rts_after_send = 0;
}
/*
* 清除rs485conf结构体中的flags成员的一些标志位,包括全双工通信标志(SER_RS485_RX_DURING_TX)启用标志(SER_RS485_ENABLED)和发送后RTS标志 *(SER_RS485_RTS_AFTER_SEND)
* 然后,设置RTS在发送时激活的标志(SER_RS485_RTS_ON_SEND)。
*/
rs485conf->flags &= ~(SER_RS485_RX_DURING_TX | SER_RS485_ENABLED |
SER_RS485_RTS_AFTER_SEND);
rs485conf->flags |= SER_RS485_RTS_ON_SEND;
if (device_property_read_bool(dev, "rs485-rx-during-tx")) //如果设备属性中存在rs485-rx-during-tx,则设置全双工通信标志。
rs485conf->flags |= SER_RS485_RX_DURING_TX;
if (device_property_read_bool(dev, "linux,rs485-enabled-at-boot-time"))//如果设备属性中存在linux,rs485-enabled-at-boot-time,则设 rs485conf->flags |= SER_RS485_ENABLED; //置启用标志。
/*如果设备属性中存在rs485-rts-active-low,则清除RTS在发送时激活的标志,并设置发送后RTS标志。这表示RTS信号在发送数据后是激活的,且如果RTS是低 *电平有效,则需要这样的配置。
*/
if (device_property_read_bool(dev, "rs485-rts-active-low")) {
rs485conf->flags &= ~SER_RS485_RTS_ON_SEND;
rs485conf->flags |= SER_RS485_RTS_AFTER_SEND;
}
}
所以rs485默认是发送高电平有效只有设置了rs485-rts-active-low属性才会设置SER_RS485_RTS_AFTER_SEND标志。
这样就完成了struct serial_rs485结构体里面的内容。接下来就是如何去配置他们了。
imx_uart_rs485_config
static int imx_uart_rs485_config(struct uart_port *port,
struct serial_rs485 *rs485conf)
{
struct imx_port *sport = (struct imx_port *)port;
u32 ucr2;
/* 将 delay_rts_before_send 和 delay_rts_after_send 强制设为 0,表明当前驱动未实现 RTS 信号的延迟控制功能。
* 原因:硬件可能不支持或驱动尚未实现这些特性。
*/
rs485conf->delay_rts_before_send = 0;
rs485conf->delay_rts_after_send = 0;
/* RS485 使能的条件检查 */
if (!sport->have_rtscts && !sport->have_rtsgpio) //如果两种都不支持则强制清除 SER_RS485_ENABLED 标志,禁用 RS485 模式
rs485conf->flags &= ~SER_RS485_ENABLED;
if (rs485conf->flags & SER_RS485_ENABLED) {
/* 硬件支持 RTS/CTS(非 GPIO 控制)。
* 用户未设置 SER_RS485_RTS_ON_SEND(即 RTS 在发送期间不激活)。
* 设置 SER_RS485_RX_DURING_TX,允许在发送期间接收数据。
*需要确保在没开启rs485的时候开启双工模式,这也就是为什么一开始没有配置为rs485模式时为什么串口需要加上uart-has-rtscts
* 在半双工 RS485 中,某些场景需要监听总线冲突(如 Modbus 主从通信)。
*/
if (sport->have_rtscts && !sport->have_rtsgpio &&!(rs485conf->flags & SER_RS485_RTS_ON_SEND))
rs485conf->flags |= SER_RS485_RX_DURING_TX;
/* 禁用发送器并设置 RTS 状态 */
ucr2 = imx_uart_readl(sport, UCR2);
if (rs485conf->flags & SER_RS485_RTS_AFTER_SEND)
imx_uart_rts_active(sport, &ucr2);
else
imx_uart_rts_inactive(sport, &ucr2);
imx_uart_writel(sport, ucr2, UCR2);
}
/*在没有使能rs485时确保接收功能启用 ,并保存配置*/
if (!(rs485conf->flags & SER_RS485_ENABLED) || rs485conf->flags & SER_RS485_RX_DURING_TX)
imx_uart_start_rx(port);
port->rs485 = *rs485conf; //保存配置
return 0;
}
serial_omap_probe_rs485函数只是将设备树里面的位图信息获取出来,存放在uart_omap_port结构体中,imx_uart_rs485_config在这个函数中才真正去配置了rs485。
串口的发送过程分析:
imx_uart_start_tx
//只保留了485部分
static void imx_uart_start_tx(struct uart_port *port)
{
struct imx_port *sport = (struct imx_port *)port;
u32 ucr1;
if (port->rs485.flags & SER_RS485_ENABLED) { //当设置了flags为SER_RS485_ENABLED才会启动485功能
u32 ucr2;
ucr2 = imx_uart_readl(sport, UCR2); //从串口控制寄存器中读取配置值
if (port->rs485.flags & SER_RS485_RTS_ON_SEND) //判断有没有设置高电平有效属性,如果有就激活rts信号,激活后将之前绑定的引脚设为高电平
imx_uart_rts_active(sport, &ucr2); //设置 UCR2_CTS (串口控制寄存器)位会拉高 RTS 引脚。
else
imx_uart_rts_inactive(sport, &ucr2);
imx_uart_writel(sport, ucr2, UCR2); //将配置值写入串口控制寄存器,启动传输
if (!(port->rs485.flags & SER_RS485_RX_DURING_TX)) //如果不支持全双工的模式则停止接收数据
imx_uart_stop_rx(port);
if (!sport->dma_is_enabled) {
u32 ucr4 = imx_uart_readl(sport, UCR4);
ucr4 |= UCR4_TCEN;
imx_uart_writel(sport, ucr4, UCR4);
}
}
}
/*
4. 完整的 RTS GPIO 控制流程
1.设备树解析:
从设备树中获取 RTS GPIO 引脚,并绑定到 sport->gpios。
2.发送时激活 RTS:
调用 imx_uart_rts_active。
如果 sport->gpios 存在,调用 mctrl_gpio_set 设置 RTS GPIO 为高电平。
如果 sport->gpios 不存在,通过设置 UCR2_RTSEN 控制硬件寄存器。
3.发送完成后恢复 RTS:
调用 imx_uart_rts_inactive。
如果 sport->gpios 存在,调用 mctrl_gpio_set 设置 RTS GPIO 为低电平。
如果 sport->gpios 不存在,通过清除 UCR2_RTSEN 控制硬件寄存器。
*/
(1) 发送完成中断触发
当 UART 发送缓冲区空(即最后一个字节已移出发送移位寄存器)时,硬件会触发 发送完成中断,中断处理函数为 imx_uart_int
:
// drivers/tty/serial/imx.c
static irqreturn_t imx_uart_int(int irq, void *dev_id) {
struct imx_port *sport = dev_id;
u32 usr1, usr2;
usr1 = imx_uart_readl(sport, USR1);
usr2 = imx_uart_readl(sport, USR2);
// 处理发送中断
if (usr1 & USR1_TRDY) { // 发送就绪中断(发送缓冲区空)
if (sport->tx_state == TX_STATE_ACTIVE) {
// 继续发送剩余数据(若有)
imx_uart_transmit_buffer(sport);
} else {
// 发送完成,停止发送
imx_uart_stop_tx(&sport->port);
}
}
return IRQ_HANDLED;
}
(2) 发送完成的硬件标志
i.MX UART 控制器通过 USR2
寄存器的 USR2_TXDC
位表示 发送移位寄存器已空(Transmitter Complete):
// 当 USR2_TXDC = 1 时,表示最后一个字节已从移位寄存器发出 if (imx_uart_readl(sport, USR2) & USR2_TXDC) { // 发送彻底完成 }
2. 方向切换的实现
方向切换的核心逻辑在 发送完成后的中断处理 和 imx_uart_stop_tx
函数 中实现,尤其是针对 RS485 模式。
(1) 发送完成后调用 imx_uart_stop_tx
在发送完成中断中,若所有数据已发送完毕(无剩余数据),会调用 imx_uart_stop_tx
:
// drivers/tty/serial/imx.c
static void imx_uart_stop_tx(struct uart_port *port) {
struct imx_port *sport = (struct imx_port *)port;
u32 ucr1;
// 禁用发送就绪中断
ucr1 = imx_uart_readl(sport, UCR1);
imx_uart_writel(sport, ucr1 & ~UCR1_TRDYEN, UCR1);
// RS485 模式处理
if (port->rs485.flags & SER_RS485_ENABLED) {
// 检查是否发送彻底完成(USR2_TXDC = 1)
if (imx_uart_readl(sport, USR2) & USR2_TXDC) {
u32 ucr2 = imx_uart_readl(sport, UCR2);
// 拉低 RTS(切换回接收模式)
if (port->rs485.flags & SER_RS485_RTS_AFTER_SEND)
imx_uart_rts_active(sport, &ucr2); // 保持 RTS 激活(特殊场景)
else
imx_uart_rts_inactive(sport, &ucr2); // 拉低 RTS
imx_uart_writel(sport, ucr2, UCR2);
// 启动接收功能
imx_uart_start_rx(port);
// 禁用发送完成中断
u32 ucr4 = imx_uart_readl(sport, UCR4);
imx_uart_writel(sport, ucr4 & ~UCR4_TCEN, UCR4);
}
}
}
(2) 关键操作
-
拉低 RTS:通过修改
UCR2
寄存器控制 RTS 信号,禁用发送器。 -
启动接收:调用
imx_uart_start_rx
启用接收中断或 DMA。 -
禁用发送中断:清除
UCR1_TRDYEN
和UCR4_TCEN
位,停止发送相关中断。 -
4. 关键代码总结
函数/寄存器 作用 imx_uart_int()
中断处理函数,检测发送完成标志( USR1_TRDY
和USR2_TXDC
)。imx_uart_stop_tx()
停止发送,拉低 RTS(RS485 模式),启动接收。 imx_uart_start_rx()
启用接收中断或 DMA,确保接收功能恢复。 USR2_TXDC
硬件标志位,表示发送移位寄存器已空(发送彻底完成)。
串口的接受过程详解
当UART接收到数据时,会触发接收中断----->在中断处理程序中,驱动会读取接收缓冲区中的数据,------->然后通过TTY子系统传递到线路规程,--------->最终到达 用户空间的应用程序。
UART驱动的接收流程大致如下:
-
硬件接收到数据,触发中断。
-
中断处理程序读取数据,存入环形缓冲区。
-
通知TTY层有数据可读,唤醒等待的读进程。
-
用户空间通过read系统调用从TTY设备读取数据。
下面来分析一下这个过程。首先初始化时,通过 set_termios
函数配置 UART 的 RS485 参数:
// drivers/tty/serial/imx.c
static void imx_uart_set_termios(struct uart_port *port, struct ktermios *termios, struct ktermios *old) {
// 配置波特率、数据位等
// 启用 RS485 模式
if (port->rs485.flags & SER_RS485_ENABLED) {
// 配置 RTS 信号方向控制
if (port->rs485.flags & SER_RS485_RTS_AFTER_SEND) {
// 发送后保持 RTS 激活(根据具体硬件逻辑)
} else {
// 发送后关闭 RTS
}
}
}
2. 启动接收功能
在 RS485 模式下,发送完成后需要显式启动接收。通常在发送完成中断中调用 imx_uart_stop_tx
,并在其中启用接收:
// drivers/tty/serial/imx.c
static void imx_uart_stop_tx(struct uart_port *port)
{
struct imx_port *sport = (struct imx_port *)port;
u32 ucr1;
if (sport->dma_is_txing)
return;
ucr1 = imx_uart_readl(sport, UCR1);
imx_uart_writel(sport, ucr1 & ~UCR1_TRDYEN, UCR1);
if (port->rs485.flags & SER_RS485_ENABLED &&
imx_uart_readl(sport, USR2) & USR2_TXDC) {
u32 ucr2 = imx_uart_readl(sport, UCR2), ucr4;
if (port->rs485.flags & SER_RS485_RTS_AFTER_SEND) // 关闭发送器,拉高 RTS
imx_uart_rts_active(sport, &ucr2);
else
imx_uart_rts_inactive(sport, &ucr2); // 关闭发送器,拉低 RTS,就和开启反着来
imx_uart_writel(sport, ucr2, UCR2);
imx_uart_start_rx(port); //开启串口接受功能
ucr4 = imx_uart_readl(sport, UCR4);
ucr4 &= ~UCR4_TCEN;
imx_uart_writel(sport, ucr4, UCR4);
}
}
imx_uart_start_rx
会启用接收中断或 DMA:
static void imx_uart_start_rx(struct uart_port *port) {
struct imx_port *sport = (struct imx_port *)port;
unsigned int ucr1, ucr2;
// 启用接收中断
ucr1 = imx_uart_readl(sport, UCR1);
ucr2 = imx_uart_readl(sport, UCR2);
ucr2 |= UCR2_RXEN; // 启用接收就绪中断
// 或者启用 DMA 接收
if (sport->dma_is_enabled) {
ucr1 |= UCR1_RXDMAEN | UCR1_ATDMAEN;// 启用 DMA 接收
} else {
ucr1 |= UCR1_RRDYEN; // 启用接收功能
ucr2 |= UCR2_ATEN;
}
/* Write UCR2 first as it includes RXEN */
imx_uart_writel(sport, ucr2, UCR2);
imx_uart_writel(sport, ucr1, UCR1);
}
3. 数据接收中断处理
当 UART 接收到数据时,硬件触发接收中断,进入中断服务程序(ISR):
// drivers/tty/serial/imx.c
//简略了内容
static irqreturn_t imx_uart_int(int irq, void *dev_id) {
struct imx_port *sport = dev_id;
u32 usr1, usr2;
// 读取中断状态寄存器
usr1 = imx_uart_readl(sport, USR1);
usr2 = imx_uart_readl(sport, USR2);
// 处理接收中断
if (usr1 & USR1_RRDY) { // 接收就绪中断
imx_uart_rxint(dev_id); // 处理接收数据
handled = IRQ_HANDLED;
}
// 其他中断(如发送完成、错误等)处理
// ...
return handled;
}
在 imx_uart_rxint
中读取接收数据:
static void imx_uart_rxint(struct imx_port *sport) {
// 从 FIFO 或寄存器中读取数据
while (!(imx_uart_readl(sport, USR2) & USR2_RDR) {
char ch = imx_uart_readl(sport, URXD0);
// 将数据存入 TTY 缓冲区
tty_insert_flip_char(&sport->port.state->port, ch, TTY_NORMAL);
}
// 通知 TTY 层数据可用
tty_flip_buffer_push(&sport->port.state->port);
}
4. TTY 子系统传递数据到用户空间
TTY 子系统负责将数据从驱动层传递到用户空间:
-
数据缓冲:驱动通过
tty_insert_flip_char
将数据存入 TTY 的flip buffer
。 -
数据推送:调用
tty_flip_buffer_push
通知 TTY 层有新数据。 -
用户读取:用户空间通过
read
系统调用从 TTY 设备读取数据:
// 用户空间代码示例 char buf[256]; int n = read(uart_fd, buf, sizeof(buf));
. 关键代码路径总结
-
RS485 发送完成:
-
发送完成中断触发
imx_uart_stop_tx
。 -
在
imx_uart_stop_tx
中拉低 RTS,并调用imx_uart_start_rx
启动接收。
-
-
接收启动:
-
imx_uart_start_rx
启用接收中断(UCR1_RRDYEN
)或 DMA。
-
-
数据接收:
-
接收中断触发
imx_uart_int
,调用imx_uart_rxint
读取数据。 -
数据通过
tty_insert_flip_char
和tty_flip_buffer_push
传递到 TTY 层。
-
-
用户空间读取:
-
用户调用
read
从/dev/ttymxcX
设备读取数据。
-
. RS485 模式下的特殊处理
-
方向切换:在发送完成后必须及时切换回接收模式,否则会丢失数据。
-
RTS 控制:通过
imx_uart_rts_active
和imx_uart_rts_inactive
函数控制 RTS 信号。 -
超时处理:某些场景下需要启用接收超时中断(例如
UCR4_OREN
),防止长时间无数据导致阻 塞。
应用程序代码
这是基于libmodbus库编写的应用程序,在后面我会出一期手动编写modbus协议的版本。
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#include <errno.h>
#include <unistd.h>
#define MODBUS_DEVICE "/dev/ttymxc1" // 串口设备
#define MODBUS_BAUD 9600 // 波特率
#define MODBUS_PARITY 'N' // 校验位(N: None, E: Even, O: Odd)
#define MODBUS_DATA_BITS 8 // 数据位
#define MODBUS_STOP_BITS 1 // 停止位
#define SLAVE_ID 1 // 从机地址
#define REGISTER_ADDRESS 0x0002 // 寄存器起始地址
#define REGISTER_COUNT 2 // 寄存器数量
int main()
{
modbus_t *ctx;
uint16_t tab_reg[REGISTER_COUNT];
int rc;
// 创建 Modbus RTU 上下文
ctx = modbus_new_rtu(MODBUS_DEVICE, MODBUS_BAUD, MODBUS_PARITY, MODBUS_DATA_BITS, MODBUS_STOP_BITS);
if (ctx == NULL)
{
fprintf(stderr, "Unable to create the libmodbus context\n");
return -1;
}
// 设置从机地址
modbus_set_slave(ctx, SLAVE_ID);
// 启用调试模式
modbus_set_debug(ctx, TRUE);
// 设置超时时间
struct timeval timeout;
timeout.tv_sec = 2; // 5 秒超时
timeout.tv_usec = 0;
while (1)
{
modbus_set_response_timeout(ctx, timeout.tv_sec, timeout.tv_usec);
// 打开连接
if (modbus_connect(ctx) == -1)
{
fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 读取寄存器
rc = modbus_read_registers(ctx, REGISTER_ADDRESS, REGISTER_COUNT, tab_reg);
if (rc == -1)
{
fprintf(stderr, "Read failed: %s\n", modbus_strerror(errno));
}
else
{
// 打印寄存器值
for (int i = 0; i < rc; i++)
{
printf("reg[%d]=%f\n", i, tab_reg[i] * 0.1);
}
}
sleep(2);
}
// 关闭连接
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
需要注意这里需要交叉编译安装modbus库,我安装的是libmodbus-3.1.10,具体安装过程可以去搜安装教程。还有就是以上的串口设置需要根据具体的传感器数据手册进行调整。
最终结果
最后也是成功读取到了传感器的数据。需要注意的还有就是硬件的连接。